스프링 핵심 기술-02.@Autowire


IoC 컨테이너 3부 : @Autowire

이번 장도 마찬가지로 이전 포스트와 겹치는 내용들이 많았지만,
중간중간 에러 사례를 보여주며 자세한 옵션들 등 도움이 되는 추가 정보들이 있었던 것 같다.
자세한 설명은 이전 포스트를 참고하면 될 것 같다.

@Autowired?

필요한 의존 객체의 “타입”에 해당하는 빈을 찾아 주입한다.
-required: 기본값은 true (따라서 못 찾으면 애플리케이션 구동 실패)

-사용할 수 있는 위치

1.생성자 (스프링 4.3 부터는 생략 가능)
2.세터
3.필드

빈 주입 경우의 수

1.해당 타입의 빈이 없는 경우
2.해당 타입의 빈이 한 개인 경우
3.해당 타입의 빈이 여러 개인 경우

하나씩 살펴보자면
1. 생성자, 혹은 setter로 의존성 주입 시에 만약 북 레포지터리가 등록되지 않은 빈이라면?

생성자 방식

@Service
public class BookService {

    BookRepository bookRepository;

    @Autowired
    public BookService(BookRepository bookRepository) {
      this.bookRepository = bookRepository;
    }
}
Description:
Parameter 0 of method setBookRepository in me.whiteeship.springapplicationcontext.BookService required a bean of type 'me.whiteeship.springapplicationcontext.BookRepository' that could not be found.

Action:
Consider defining a bean of type 'me.whiteeship.springapplicationcontext.BookRepository' in your configuration.

Description:
이 생성자의 0번째 파라미터에 설정한 북서비스에 필요한 BookRepository 빈이 없다
Action:
따라서 레포지터리 타입을 빈으로 정의해라.

setter 방식

@Service
public class BookService {

    BookRepository bookRepository;

    @Autowired
    public void setBookRepository(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }
}

역시 같은 오류가 발생한다.

***************************
APPLICATION FAILED TO START
***************************

Description:
Parameter 0 of method setBookRepository in me.whiteeship.springapplicationcontext.BookService required a bean of type 'me.whiteeship.springapplicationcontext.BookRepository' that could not be found.

여기서 알 수 있는게 한 가지 더 있는데,
이 케이스를 보면
생성자 주입은 빈(BookService)을 만들다가 빈에 필요한 다른 의존성이었던 빈(BookRepository)이 없어서 실패했구나..
라고 명확히 보이고 추정을 할 수 있습니다.

그런데 setter 방식의 경우에 클래스를 다시 한번 보면,
setBookRepository를 호출을 한 것도 아닌데, BookService 자체의 인스턴스는 생성을 할 수 있지 않나? 라는 생각을 할 수 있습니다.
이 말은 맞는 말인데, 적어도 BookService 자체는 만들 수 있는데
@Autowired 해당 어노테이션을 사용했기 때문에, 해당 빈(BookService)을 만들때부터 의존성을 주입을 시도합니다.
즉, 그 과정이 실패하기 때문에 에러가 발생하는 것입니다.

그렇다면 이걸 해결할 수 있는 방법이 있는데,
난 의존성 무조건 필요하지는 않다. 라는 설정을 통해 정상적으로 실행할 수 있습니다.

@Service
public class BookService {

    BookRepository bookRepository;

    @Autowired(required = false) // 의존성 주입이 꼭 필요한건 아니다, default 는 true
    public void setBookRepository(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }
}

@Autowired(required = false) 해당 설정을 활용할 수 있습니다.
(디폴트는 true)

true의 경우 의존성이 없거나 해당하는 빈의 타입을 못찾거나, 의존성 주입을 할 수 없는 경우에는 에러 및 구동 불가

2.해당 타입의 빈이 한 개인 경우 문제 없이 실행이 될 거고

3.만약 의존성을 주입하는 의존성 타입이 두개가 있을 경우는?

같은 타입의 빈이 여러개 일 때

3-1.@Primary
3-2.해당 타입의 빈 모두 주입 받기
3-3.@Qualifier

북 리퍼지토리 타입이 2개일 경우

public interface BookRepository {
}

@Repository
public class JaeukRepository implements BookRepository {
}

@Repository
public class MyBookRepository implements BookRepository{
}

BookRepository 를 인터페이스로 바꾸고 BookRepository 타입인 JaeukRepository와 MyBookRepository 를 만든 경우

***************************
APPLICATION FAILED TO START
***************************

Description:
Parameter 0 of method setBookRepository in me.whiteeship.springapplicationcontext.BookService required a single bean, but 2 were found:
	- jaeukRepository: defined in file [/Users/jaeuk/IdeaProjects/springapplicationcontext/target/classes/me/whiteeship/springapplicationcontext/JaeukRepository.class]
	- myBookRepository: defined in file [/Users/jaeuk/IdeaProjects/springapplicationcontext/target/classes/me/whiteeship/springapplicationcontext/MyBookRepository.class]


Action:
Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed

이렇게 된다면 스프링은 해당하는 타입(BookRepository)의 둘 중에서 어떤걸 원하는지 알 수 없기 때문에 주입을 해줄 수 없습니다.

에러메세지 참조
BookRepository 타입이 두 개가 발견되었다.
-JaeukRepository
-MyBookRepository
둘중에 어떤걸 써야할지 모르겠다.

추천 액션은
1.@Primary 붙여서 너가 더 사용하고 싶은 것을 마킹해라.

2.모든 빈을 다 주입받아라.

3.@Qualifier으로 너가 뭘 원하는지 마킹을 해라

1.@Primary

@Repository @Primary
  public class JaeukRepository implements BookRepository {
}

여러 개의 빈을 주입해야 할 경우 난 JaeukRepository을 자주 사용할 거라고 마킹.

2.여러 개의 빈을 다 받아라

@Service        
public class BookService {

    List<BookRepository> bookRepositories;

    @Autowired
    public void setBookRepository(List<BookRepository> bookRepositories) {
        this.bookRepositories = bookRepositories;
    }
}

3.@Qualifier

@Service
public class BookService {

    BookRepository bookRepository;

    @Autowired @Qualifier(value = "jaeukRepository")
    public void setBookRepository(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }
}

근데 @Qualifier 보다는 @Primary이 Type-Safe하니 @Primary을 더 추천.

또 @Autowired는
빈을 관리할때 기본적으로 타입도 보지만, 이름도 같이 봅니다.
즉 같은 타입이라면 빈 이름으로 시도, 같은 이름의 빈 찾으면 해당 빈 사용. 같은 이름 못 찾으면 실패하는 구조입니다.

따라서 별로 추천하는 방법은 아니지만 이런 방식으로도 구현이 가능합니다.

@Service
public class BookService {

    BookRepository bookRepository;

    @Autowired
    public void setBookRepository(BookRepository jaeukRepository) {
        this.bookRepository = jaeukRepository;
    }
}

jaeukRepository 이름으로 판별해서 넣어줍니다. 가능은 하지만 추천하지 않고, @Primary을 더 추천합니다.

  • 참고사항

    @Controller, @Repository 등
    각 어노테이션은 이름에 맞게 맞춰 사용하는게 좋다.
    나중에 AOP나 공통된 기능을 적용할 수도 있고, 새로운 기능을 추가하거나 aspectJ 사용하기에 좋다.

@Autowired의 동작 원리

키워드

  • 빈 라이프사이클(Bean LifeCycle)
  • BeanPostProcessor

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

  • AutowiredAnnotationBeanPostProcessor extends BeanPostProcessor

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

빈은 만들어진 다음(빈의 인스턴스가 생성된 다음) 생성된 빈이 초기화가 되는 빈의 초기화 라이프 사이클이 존재한다.
BeanPostProcessor는 이 빈의 초기화 라이프 사이클의 이전/이후에 부가적인 작업을 할 수 있는 또 다른 라이프사이클의 콜백 메서드를 제공한다.
(=빈이 초기화 되기 전,후 동작할 수 있는 메서드를 제공)

BeanFactory 공식문서
BeanPostProcesser
AutowiredAnnotationBeanPostProcessor

(빈의 초기화 라이프사이클 중 11~14)
11.postProcessBeforeInitialization methods of BeanPostProcessors (BeanPostProcessors의 초기화 전처리 메서드) 12.InitializingBean’s afterPropertiesSet 13.a custom init-method definition 14.postProcessAfterInitialization methods of BeanPostProcessors (BeanPostProcessors의 초기화 후처리 메서드)

보이는 대로 BeanPostProcessors는
11.postProcessBeforeInitialization(초기화 전)
14.postProcessAfterInitialization(초기화 후) 두 가지 메서드를 제공해준다.

이 중 11.postProcessBeforeInitialization(초기화 전) 메서드를 활용해서 이니셜라이션 전에 AutowiredAnnotationBeanPostProcessor 얘가 @Autowired를 처리해 주는 것이다. (=@Autowired 해당하는 빈을 찾아서 주입을 해 준다.)

또한 14.postProcessAfterInitialization(초기화 후) 메서드는 두 가지 방법으로 구현이 가능하다.

  1. 어노테이션(@) 사용
    @Service
    public class BookService {
    
     BookRepository bookRepository;
    
     @Autowired
     public void setBookRepository(BookRepository jaeukRepository) {
         this.bookRepository = jaeukRepository;
     }
        
     @PostConstruct // 이런 어노테이션으로 정의를 해 줄 수도 있고
     public void setUp() { // 이 빈이 만들어진 다음(=빈의 초기화 라이프 사이클 이후) 해야할 일 정의 가능 
         System.out.println(bookRepository.getClass());
     }
    }
    
    class me.whiteeship.springapplicationcontext.JaeukRepository
    

2.InitializingBean 인터페이스

@Service
public class BookService implements InitializingBean {

    BookRepository bookRepository;

    @Autowired
    public void setBookRepository(BookRepository jaeukRepository) {
        this.bookRepository = jaeukRepository;
    }

    @Override
    public void afterPropertiesSet() throws Exception { // 동일 = 빈 인스턴스가 만들어진 다음 부가적인 작업
        
    }
}

두 가지 방법 다 동일하다. (빈 인스턴스가 만들어진 다음 부가적인 작업)
추가적으로 빈의 초기화 과정 중에 실행되는 것이므로 구동 중에 로그가 찍힌다. (완전히 구동되기 전에 이미 실행된다)

전체적으로 살펴보면

1.IoC 컨테이너(어플리케이션 컨텍스트)는 자기 안에 등록이 되어 있는 AutowiredAnnotationBeanPostProcessor를 찾는다.
2.AutowiredAnnotationBeanPostProcessor는 BeanPostProcessor의 구현체이다.
3.AutowiredAnnotationBeanPostProcessor는 다른 일반적인 빈들을 찾아서,
BeanPostProcessor에 들어있는 어노테이션을 처리하는 로직을 다른 일반적인 빈들한테 모두 적용을 한다.




© 2019. by jaeuk