[스프링 프레임워크 핵심기술] @Autowired와 생성자 주입을 사용해야하는 이유

Dayeon myeong·2021년 2월 20일
0

@Autowired

생성자, 세터 등의 방법으로 의존성 주입하려 할 때, 필요한 의존 객체의 타입에 해당하는 빈을 찾아 주입해주는 어노테이션

  • @Autowired의 required: 기본값은 true (따라서 못 찾으면 애플리케이션 구동 실패)

빈 주입 방법

Spring에서 등록된 Bean을 사용하기 위해 DI(의존성 주입)을 처리하는 방법은 크게 3가지가 있다.

  • Field 주입
  • Setter 주입
  • 생성자 주입 (스프링 4.3부터는 @Autowired 어노테이션 생략 가능)

Spring 3.x버젼까지만 해도 Setter Inject을 권장하였으나,
최근에는 순환참조, Coupling등이 문제로 인해서 Spring 4.3 이후 버젼 부터는 Contructor Inject를 권장하고 있다.

생성자 주입

현재 가장 권장하고 있는 방법. 하나의 생성자가 존재시 기존 Field 주입의 단점을 극복해낸 패턴

만약 BookRepository가 빈으로 등록이 안되어있다면 직관적으로 빈이 등록이 되어있지 않은 것을 알 수 있고 에러가 난다.

생성자 주입 장점

  • null을 주지않는 한 nullPointerException이 발생하지 않는다.

  • 의존관계 주입을 하지 않은 경우에는 BookService 인스턴스를 생성할 수 없다. 즉, 의존 관계에 대한 내용을 외부로 노출시킴으로써 컴파일 타임에 오류를 잡아낼 수 있다.

  • final로 선언이 가능하다. final은 누군가가 BookService 내부에서 BookRepository 객체를 바꿔칠 수 없게한다. (final로 선언된 변수는 반드시 선언과 함께 초기화되어야하므로 생성자 주입만 가능)

  • 순환 참조인 경우 StackOverflowError을 발생시킴으로써 순환 의존성을 알 수 있다. 필드 주입과 setter 주입은 객체 생성시점에는 순환참조가 일어나는지 아닌지 발견할 수 있는 방법이 없다.(필드 주입과 setter 주입은 먼저 빈을 생성한후, 주입하려는 빈을 찾아 주입한다.) 생성자 주입에서는 컨테이너가 빈을 생성하는 시점에서 객체 생성에 사이클 관계가 생기기 때문이다.(생성자 주입은 먼저 생성자의 인자에 사용되는 빈을 찾거나 빈 팩토리를 만듭니다. 그 후에 찾은 인자 빈으로 주입하려는 빈의 생성자를 호출합니다. 즉, 먼저 빈을 생성하지 않고 주입하려는 빈을 먼저 찾습니다.)

  • 단위 테스트 작성하기 좋다: 필드 주입과 setter 주입에서는 빈이 주입되어있으면 mockito로 목킹하여 가짜 객체 생성 후 테스트를 해야한다. 생성자 주입의 경우는 단순히 원하는 객체를 생성한 후, 생성자에 넣어주면 된다.

순환 참조 : 서로서로 주거니 받거니 함수를 반복하면서 끊엄없이 호출. A->B 참조하면서 B->A 참조하는 경우

Lombok 뿌리기

@Service
@AllArgsConstructor
public class BookService {

    private BookRepository bookRepository;

}

immutability 이슈까지 해결하고 싶다면 (final 사용)

@Service
@RequiredArgsConstructor
public class BookService {

    private final BookRepository bookRepository;

}

Setter 주입

만약 BookRepository가 빈으로 등록이 안되어있다면 BookService 인스턴스는 만들 수 있지만 BookRepository는 의존성 주입에 실패한다.

	@Autowired(required=false)
	...

그러므로 위와같이 required = false 옵션을 두어 BookRepository 빈이 없어도 된다라고 명시해줘야 한다.

setter 주입 단점


class BookService {

	@Autowired(required=false)
    private BookRepository bookRepository;
    
    public void setBookRepository(BookRepository bookRepository) {
    	this.bookRepository = bookRepository;
    }
    
    public void callRepository() {
    	bookRepository.doSomthing();
    }
}

위 코드에서는 BookService는 BookRepository를 주입하지 않아도 인스턴스 생성이 가능하다. 그런데 callRpository()메서드에서 bookRepository.doSomthing()을 호출하고 있으므로

  • nullPointerException이 발생한다.
  • 주입이 필요한 객체가 주입이 되지 않아도 얼마든지 객체를 생성할 수 있다.

위와 같은 문제가 있으므로 이를 해결한 방법이 생성자 주입이다.

Field 주입

가장 간단한 방법으로 Bean으로 등록된 객체를 사용하고자 하는 클래스에 Field로 선언한 뒤 @Autowired키워드를 붙여주면 자동으로 주입된다.

만약 BookRepository가 빈으로 등록이 안되어있는 상황이 있을 수 있기 때문에 required=false를 둠.

Field 주입의 단점

매우 간단한 방법이지만, 단점이 많아서 권장되고 있지 않은 방법

  • 단일 책임의 원칙 위반 : 의존성을 주입하기가 너무 쉬어서 빈을 막 추가할 수 있다. 생성자 주입을 사용하면 다른 주입 방식에 비해 생성자 파라미터가 많아짐과 동시에 하나에 클래스가 많은 책임을 떠안는다는 것을 확인가능하기 때문에 생성자 주입 방식을 추천한다.
  • 의존성이 숨는다 : DI(Dependency Injection) 컨테이너를 사용한다는 것은 클래스가 자신의 의존성만 책임진다는게 아니다. 제공된 의존성 또한 책임진다. 그래서 클래스가 어떤 의존성을 책임지지 않을 때, 메서드나 생성자를 통해(Setter나 Contructor) 확실히 커뮤니케이션이 되어야한다. 하지만 Field Injection은 숨은 의존성만 제공해준다.
  • DI 컨테이너의 결합성과 테스트 용이성 : DI 프레임워크의 핵심 아이디어는 관리되는 클래스가 DI 컨테이너에 의존성이 없어야 한다. 즉, 필요한 의존성을 전달하면 독립적으로 인스턴스화 할 수 있는 단순 POJO여야한다. DI 컨테이너 없이도 유닛테스트에서 인스턴스화 시킬 수 있고, 각각 나누어서 테스트도 할 수 있다. 컨테이너의 결합성이 없다면 관리하거나 관리하지 않는 클래스를 사용할 수 있고, 심지어 다른 DI 컨테이너로 전환할 수 있다. 하지만, Field Injection을 사용하면 필요한 의존성을 가진 클래스를 곧바로 인스턴스화 시킬 수 없다.
  • 불변성(Immutability) : Constructor Injection과 다르게 Field Injection은 final을 선언할 수 없다. 그래서 객체가 변할 수 있다.
  • 순환 참조 : constructor Injection에서 순환 의존성을 가질 경우 BeanCurrentlyCreationExeption을 발생시킴으로써 순환 의존성을 알 수 있다.

해당 타입의 빈이 여러 개인 경우

@Qualifier : 빈 이름으로 주입

동일한 Class의 Bean이 여러개 존재하는 경우 BookService는 의존성 주입에 실패한다.
그러므로 위와 같이 @Qualifier("...")를 붙여주어 bean 이름을 지정하여 주입 받는 것이 가능하다.
같은 이름을 못찾으면 에러가 빈 주입에 실패한다.

BookRepository 인터페이스를 상속받는 MyBookReposiroty, YourBookRepository가 있을 때 @Qualifier를 쓰면 MyBookRepository와 YourBookRepository의 스몰케이스로 빈 이름을 찾는다.

@Autowired @Qualifier("myBookRepository")// 스몰케이스인 빈 이름에 맞는 클래스를 찾음

@Primary : 주로 사용할 빈 설정

BookRepository 인터페이스를 상속받는 MyBookReposiroty, YourBookRepository가 있을 때
주로 사용할 빈을 @Primary 어노테이션을 붙여 주입가능하다.

해당 타입의 빈 모두 주입받기

여러개의 타입 중 필드 이름과 동일한 빈을 주입받기

필드이름 myBookRepository와 동일한 MyBookRepository를 주입받아준다.

어떤 빈이 주입받는지 확인하고 싶을 때

어떤 빈이 BookService에 주입받는지 확인하고 싶을 때 ApplicationRuunner를 사용한다.


printBookRepository()를 통해 어떤 빈이 주입되었는지 출력되었다.

빈 주입 동작 원리

  • BeanPostProcessor 라이프사이클인터페이스 구현체에 의해서 동작을 하는 것

BeanPostProcessor빈의 initializing(초기화) 라이프 사이클 이전, 이후에 필요한 부가 작업을 할 수 있는 라이프 사이클 콜백이다.

BeanPostProcessor : 새로 만든 빈 인스턴스를 수정 할 수 있는 라이프 사이클 인터페이스

그리고 BeanPostProcessor의 구현체인 AutowiredAnnotationBeanPostProcessor가 빈의 초기화 라이프 사이클 이전, 즉 빈이 생성되기 전에 @Autowired가 붙어있으면 해당하는 빈을 찾아서 주입해주는 작업을 하는 것이다.

AutowiredAnnotationBeanPostProcessor extends BeanPostProcessor : 스프링이 제공하는 @Autowired와 @Value 애노테이션 그리고 JSR-330의 @Inject 애노테이션을 지원하는 애노테이션 처리기.

AutowiredAnnotationBeanPostProcessor는 하나의 빈으로써 spring IoC 컨테이너에 등록되어 있다.

BeanFactory(ApplicationContext)는 BeanPostProcessor 타입의 빈 = AutowiredAnnotationBeanPostProcessor 빈을 꺼내 일반적인 빈들 = @Autowired로 의존성 주입이 필요한 빈들에게 @Autowired를 처리하는 로직을 적용한다.

빈의 initialization

ex) @PostConstruct : 해당 빈이 만들어진 후 해야할 일 정의할 수 있다.

참고
https://leejisoo860911.tistory.com/2
https://jackjeong.tistory.com/41
https://yaboong.github.io/spring/2019/08/29/why-field-injection-is-bad/

profile
부족함을 당당히 마주하는 용기

0개의 댓글