[스프링 IoC 컨테이너와 빈]_03 빈 주입 방법

6 minute read

@Autowired

필요한 의존 객체의 “타입”에 해당하는 빈을 찾아 주입한다.

@Autowired

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

사용할 수 있는 위치

​ ● 생성자 (스프링 4.3 부터는 생략 가능)
​ ● 세터
​ ● 필드

경우의 수

​ ● 해당 타입의 빈이 없는 경우
​ ● 해당 타입의 빈이 한 개인 경우
​ ● 해당 타입의 빈이 여러 개인 경우
​ ○ 빈 이름으로 시도,
​ ■ 같은 이름의 빈 찾으면 해당 빈 사용
​ ■ 같은 이름 못 찾으면 실패

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

​ ● @Primary
​ ● 해당 타입의 빈 모두 주입 받기
​ ● @Qualifier (빈 이름으로 주입)

동작 원리

​ ● 첫시간에 잠깐 언급했던 빈 라이프사이클 기억하세요?
​ ● BeanPostProcessor
​ ○ 새로 만든 빈 인스턴스를 수정할 수 있는 라이프 사이클 인터페이스
​ ● AutowiredAnnotationBeanPostProcessor extends BeanPostProcessor
​ ○ 스프링이 제공하는 @Autowired와 @Value 애노테이션 그리고 JSR-330의
​ @Inject 애노테이션을 지원하는 애노테이션 처리기.


※ [실습]

프로젝트 생성 : [Spring Initializr] > [Dependencies] : web 선택

BookService와 BookRepository 클래스를 생성 후, BookService만 @Service 로 빈으로 만들고 BookRepository는 빈으로 등록하지 않은 상태에서 BookService에서 BookRepository 의존성 주입을 해보자

@Autowired 사용할 수 있는 위치

1. 생성자로 주입

IDE에서 BookRepository로 등록된 빈이 없어서 알려주고 있지만 의도한 코드이므로 무시하고 넘어간다.

생성자 빈주입

애플리케이션을 실행하여 오류를 확인

⇒ 오류 내용 : BookService 생성자의 0번째 파라미터에 해당하는 BookRepository 빈이 없다.

⇒ 해결 방안 : BookRepository에 해당하는 빈을 정의해라

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

Description:

Parameter 0 of constructor in dev.solar.demospring51.BookService required a bean of type 'dev.solar.demospring51.BookRepository' that could not be found.


Action:

Consider defining a bean of type 'dev.solar.demospring51.BookRepository' in your configuration.


Process finished with exit code 1

BookRepository에 @Repository 를 붙여주자

@Component 어노트에션을 붙일 수도 있지만 Repository로 활용할 클래스이기 때문에 @Repository를 붙여주는 것이 좋다. 특정한 기능을 실행시킬 수도 있고, AOP에서도 사용하기 더 좋기 때문에 구분해서 쓰는 것이 더 좋다.

다시 실행하면 에러 없이 잘 주입이 되는 것을 확인할 수 있다.

2. Setter로 주입

setter 주입

BookRepository에 @Repository를 붙이지 않고 실행하면 동일하게 오류가 나는 것을 확인할 수 있다.

생성자로 주입한 경우와 Setter로 주입한 경우에 발생한 에러에서 알 수 있는 점

생성자로 주입한 경우 BookService를 만드려고 할 때 부터 에러가 나는 것은 분명하다.

Setter로 주입하는 경우, Setter를 호출할 때 해당하는 빈이 없어서 오류가 나는 것은 맞지만, BookService 인스턴스 자체는 만들 수 있는 것 아닌가?

맞는 말이지만, @Autowired 라는 어노테이션이 붙어있기 때문에 BookService 빈을 생성할 때 의존성을 주입하려고 시도한다. 따라서 실행 시 오류가 발생하는 것이다.

@Autowired 어노테이션이 필수가 아니라 옵셔널한 사항이라면 @Autowired(required = false) 로 설정해서 실행하면 에러가 발생하지 않는다.

@Service
public class BookService {

  BookRepository bookRepository;

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

3. 필드로 주입

필드 주입 시에도 @Autowired 설정을 required = false로 설정해서 옵셔널하게 지정하면 에러 없이 빈 생성이 가능하다.

@Service
public class BookService {

  @Autowired(required = false)
  BookRepository bookRepository;

}
  • 생성자 인젝션과 Setter, 필드 인젝션의 차이

    Setter와 필드 의존성 주입시에는 옵셔널하게 설정해서 해당하는 빈이 없이도 인스턴스 자체는 만들도록 할 수 있다.

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

BookRepository를 인터페이스로 수정하고, 이를 구현한 MyBookRepository와 SolarBookRepository 두 개의 구현체가 있는 경우

다음과 같이 BookService에서 BookRepository를 주입받으려고 하면 오류가 발생한다.

@Service
public class BookService {

  @Autowired
  BookRepository bookRepository;

}
  • 오류 메시지

BookService에서 필요한 bookRepository 필드에 하나의 빈이 필요한데, 해당하는 타입의 빈이 2개가 발견됐다.

myBookRepository, solarBookRepository 둘 중에 어떤 것을 써야할 지 모르겠다.

  • 추천하는 액션

    1. 여러가지 빈들 중에 @Primary를 붙여서 마킹을 해라
    2. 모든 빈을 다 받아라 3. @Qualifier로 어떤 빈이 필요한지 지정해라
Description:

Field bookRepository in dev.solar.demospring51.BookService required a single bean, but 2 were found:
	- myBookRepository: defined in file [/Users/ssun/Develop/Project/SpringProjects/study-spring/project/demospring51/target/classes/dev/solar/demospring51/MyBookRepository.class]
	- solarBookRepository: defined in file [/Users/ssun/Develop/Project/SpringProjects/study-spring/project/demospring51/target/classes/dev/solar/demospring51/SolarBookRepository.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

1. @Primary 를 붙여서 마킹(추천)

사용하고자 하는 빈에 @Primary 어노테이션을 붙인다.

@Repository @Primary
public class SolarBookRepository implements BookRepository{}

어떠한 빈이 주입됐는지 확인하기 힘드므로 ApplicationRunner 구현체를 하나 만들어 주자 (참고. 이 내용은 스프링 부트 강좌에서)

BookService에서 주입받은 BookRepository 구현체가 어떤 것이지 내용을 출력해주는 메서드 생성

@Service
public class BookService {

  @Autowired
  BookRepository bookRepository;

  public void printBookRepository() {
    System.out.println(bookRepository.getClass());
  }
}

Runner에서 해당 메서드를 호출

@Component
public class BookServiceRunner implements ApplicationRunner {

  @Autowired
  BookService bookService;

  @Override
  public void run(ApplicationArguments args) throws Exception {
    bookService.printBookRepository();
  }
}

애플리케이션을 실행하면 (자동으로) Runner들을 전부 쭉 호출해준다.

주입된 빈 클래스 확인

2. @Qualifier(빈 이름) 으로 주입

@Repository 어노테이션을 쓰면 이 빈의 id는 small case로 시작하는 빈의 이름과 동일하게 된다.

@Repository
public class SolarBookRepository implements BookRepository{} //-> 빈 id : solarBookRepository

@Qualifier("빈이름")으로 지정해주면 된다.

@Service
public class BookService {

  @Autowired @Qualifier("solarBookRepository")
  BookRepository bookRepository;

  public void printBookRepository() {
    System.out.println(bookRepository.getClass());
  }
}

하지만, 좀 더 Type Safety한 @Primary 방식을 사용할 것을 추천한다.

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

@Service
public class BookService {

  @Autowired
  List<BookRepository> bookRepositories;

  public void printBookRepository() {
    this.bookRepositories.forEach(System.out::println);
  }
}

모든 빈 주입받기

4. 빈의 이름과 동일한 필드명으로 주입받기(비추천)

@Autowired는 타입 뿐만 아니라 이름도 확인한다.

@Service
public class BookService {

  @Autowired
  BookRepository myBookRepository;

  public void printBookRepository() {
    System.out.println(myBookRepository.getClass());
  }
}

필드명

우선순위

빈과 동일한 필드명으로 적었어도 특정 빈에 @primary가 붙어있다면 해당하는 빈으로 생성된다.

동작원리

BeanPostProcessor 라는 라이프 사이클 인터페이스의 구현체에 의해서 동작한다.

BeanPostProcessor

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

  • 빈을 만들고 빈의 인스턴스를 만든 다음에 빈의 initialize 라이프 사이클이 있다.
  • 빈의 initialize 라이프 사이클 전후로 추가적인 동작을 구현할 수 있는 라이프 사이클 콜백이 BeanPostProcessor이다.

1. @PostConstruct

@Service
public class BookService {

  @Autowired
  BookRepository myBookRepository;

  public void printBookRepository() {
    System.out.println(myBookRepository.getClass());
  }

  @PostConstruct
  public void setUp() {
    //이 빈이 만들어진 다음에 해야할 동작을 정의
  }
}

2. InitializingBean 상속

  • afterPropertiesSet() 를 구현해야함
@Service
public class BookService implements InitializingBean {

  @Autowired
  BookRepository myBookRepository;

  public void printBookRepository() {
    System.out.println(myBookRepository.getClass());
  }

  @Override
  public void afterPropertiesSet() throws Exception {
    //이 빈이 만들어진 다음에 해야할 동작을 정의
  }
}

그 중 AutowiredAnnotationBeanPostProcessor 가 동작해서 @Autowired 어노테이션을 처리(해당 하는 빈을 찾아서 주입)해주는 것이다.

참고 : BeanFactory Spring Docs

AutowiredAnnotationBeanPostProcessor

따라서 기존에 생성된 빈 정보를 출력하는 printBookRepository() 코드를 @PostConstruct 으로 작성해도 동일하다.

이미 빈이 생성된 후의 라이프 사이클에서 동작하는 코드 임을 알아야 한다.

@Service
public class BookService {

  @Autowired
  BookRepository myBookRepository;

  @PostConstruct
  public void setUp() {
    System.out.println(myBookRepository.getClass());
  }
}

PostConstruct 코드

클래스 이름이 찍힌 위치가 다르다. Runner는 애플리케이션이 구동이 완료된 이후에 실행되는 반면에 @PostConstruct 라이프사이클 콜백은 InitializingBean’s afterPropertiesSet 단계에서 실행되는 것이기 때문이다.

※ 라이프 사이클

  1. BeanNameAware’s setBeanName
  2. BeanClassLoaderAware’s setBeanClassLoader
  3. BeanFactoryAware’s setBeanFactory
  4. EnvironmentAware’s setEnvironment
  5. EmbeddedValueResolverAware’s setEmbeddedValueResolver
  6. ResourceLoaderAware’s setResourceLoader (only applicable when running in an application context)
  7. ApplicationEventPublisherAware’s setApplicationEventPublisher (only applicable when running in an application context)
  8. MessageSourceAware’s setMessageSource (only applicable when running in an application context)
  9. ApplicationContextAware’s setApplicationContext (only applicable when running in an application context)
  10. ServletContextAware’s setServletContext (only applicable when running in a web application context)
  11. postProcessBeforeInitialization methods of BeanPostProcessors
  12. InitializingBean’s afterPropertiesSet <—— **
  13. a custom init-method definition
  14. postProcessAfterInitialization methods of BeanPostProcessors

BeanPostProcessor구현체 중 하나가 동작하는 것이라고 생각하면된다.

BeanFactory가 자신에게 등록되어있느 BeanPostProcessor타입의 빈을 찾는다. 구현체 중에 하나인 AutowiredAnnotationBeanPostProcessor가 빈으로 등록되어있는 것이다. AutowiredAnnotationBeanPostProcessor에 실제 어노테이션을 처리하는 로직들이 정이되어있고 이를 다른 일반적인 빈들한테 적용하는 것이다.

AutowiredAnnotationBeanPostProcessor가 빈으로 등록되어 있다는 것을 확인하는 방법

빈을 직접 가져올 수 있고, 빈을 담고 있는 ApplicationContext를 가져와서 빈을 꺼낼 수 있다.

  1. ApplicationRunner를 생성 후, ApplicationContext를 주입받아서 AutowiredAnnotationBeanPostProcessor 빈을 꺼내와 확인
@Component
public class MyRunner implements ApplicationRunner {

  @Autowired
  ApplicationContext applicationContext;

  @Override
  public void run(ApplicationArguments args) throws Exception {
    AutowiredAnnotationBeanPostProcessor bean = applicationContext.getBean(AutowiredAnnotationBeanPostProcessor.class);
    System.out.println(bean);
  }
}
  1. AutowiredAnnotationBeanPostProcessor 가 이미 빈으로 등록이 되어있으므로 @Autowired 로 바로 받아올 수 있다.
@Component
public class MyRunner implements ApplicationRunner {

  @Autowired
  AutowiredAnnotationBeanPostProcessor processor;

  @Override
  public void run(ApplicationArguments args) throws Exception {
    System.out.println(processor);
  }
}