스프링 프레임워크 Autowired 어노테이션
12 Feb 2022백기선님의 ‘스프링 프레임워크 핵심 기술’ 강의 정리
@Autowired
해당 타입의 빈이 없는 경우
BookService, BookRepository 클래스를 만들고 BookService에만 어노테이션을 추가해서 빈으로 등록하도록 한다. 빈으로 등록되지 않은 BookRepository를 빈으로 등록한 BookService에서 @Autowired를 통해 의존성을 주입받도록 하고 어플리케이션을 구동하면 어떤 결과가 나올까?
// BookRepository
public class BookRepository {}
// BookService
@Service
public class BookService {
BookRepository bookRepository;
@Autowired
public BookService(BookRepository bookRepository) {
this.bookRepository = bookRepository;
}
}
앱을 실행해보면 APPLICATION FAILED TO START 라는 문구와 함께 앱을 구동하지 못한 이유와 해결 방법을 알려준다. BookService는 BookRepository 타입의 빈을 필요로 하지만 찾을 수 없으니 이 타입의 빈을 설정에 정의하라는 내용을 확인할 수 있다. 스프링의 안내에 따라 @Repository 어노테이션으로 빈으로 등록한 후 앱을 다시 실행하면 문제없이 의존성 주입이 되는 것을 확인할 수 있다.
Description:
Parameter 0 of constructor in com.example.springcore.BookService required a bean of type 'com.example.springcore.BookRepository' that could not be found.
Action:
Consider defining a bean of type 'com.example.springcore.BookRepository' in your configuration.
위에서는 생성자를 통해서 의존성을 주입했다. 빈으로 등록되지 않은 BookRepository를 setter 메서드를 만들어서 똑같이 @Autowired로 의존성을 주입하면 이번에도 결과는 똑같이 앱을 실행할 수 없다는 안내가 나온다. 생성자 주입이 아니었기 때문에 BookService 인스턴스 자체는 만들 수 있었지만 빈으로 등록되지 않은 BookRepository를 주입 하려 했기 때문에 실패하게 되는 것이다. 만약 BookRepository의 의존성이 필수가 아닌 선택적이라면 required = false
설정을 넣어준다면 의존성 주입이 필수가 아니기 때문에 앱이 실행된다. required 설정은 setter 주입과 field 주입에서만 유효한 설정이다. 생성자 주입에서는 이 설정이 있다고 해도 파라미터 값으로 받아야 하기 때문에 빈으로 등록되지 않으면 에러가 발생한다.
Description:
Parameter 0 of method setBookRepository in com.example.springcore.BookService required a bean of type 'com.example.springcore.BookRepository' that could not be found.
Action:
Consider defining a bean of type 'com.example.springcore.BookRepository' in your configuration.
해당 타입의 빈이 여러 개인 경우
클래스로 만들었던 BookRepository를 인터페이스로 바꾸고 이를 구현한 클래스 두 개를 만들고 @Repository 어노테이션으로 빈으로 등록한다.
// BookRepository
public interface BookRepository {
}
// MyBookRepository
@Repository
public class MyBookRepository implements BookRepository{
}
// OtherBookRepository
@Repository
public class OtherBookRepository implements BookRepository{
}
그리고 BookService에서 BookRepository를 의존성 주입을 받으려고 하면 어떤 걸 가져오게 될까?
@Service
public class BookService {
@Autowired
BookRepository bookRepository;
}
MyBookRepository와 OtherBookRepository 모두 BookRepository를 구현했으니 둘 다 주입받을 수도 있지 않을까 생각했지만 스프링은 또 다시 실패 안내를 해주었다. BookService는 하나의 빈을 필요로 하지만 두 개가 발견되었으니 @Primary
또는 @Qualifier
어노테이션으로 표시를 하라고 한다.
Description:
Field bookRepository in com.example.springcore.BookService required a single bean, but 2 were found:
- myBookRepository: defined in file [/Users/Desktop/spring-core/target/classes/com/example/springcore/MyBookRepository.class]
- otherBookRepository: defined in file [/Users/Desktop/spring-core/target/classes/com/example/springcore/OtherBookRepository.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
@Primary
어노테이션은 주입 받을 빈에서 사용하는 어노테이션이다. 두 개의 빈 중에 MyBookRepository를 주입받고 싶다고 하면 여기에 어노테이션을 추가해주면 된다.
@Repository @Primary
public class MyBookRepository implements BookRepository {
}
@Qualifier
어노테이션은 의존성을 주입 받으려는 빈에서 사용한다. 어노테이션에 주입 받고 싶은 빈의 이름을 적어주면 된다. @Qualifier보다는 @Primary가 더 type-safe하므로 필요하다면 @Primary를 사용하는 것이 좋다.
@Service
public class BookService {
@Autowired @Qualifier("myBookRepository")
BookRepository bookRepository;
}
어노테이션을 추가하고 실제로 해당 빈이 주입되는지 확인하기 위해 BookServiceRunner 클래스를 만들어본다. BookService에는 bookRepository를 출력하는 메서드를 추가한다. 설정한대로 MyBookRepository가 출력되는 걸 확인할 수 있다.
// BookService
@Service
public class BookService {
@Autowired @Qualifier("myBookRepository")
BookRepository bookRepository;
public void printBookRepository() {
System.out.println(bookRepository.getClass());
}
}
// BookServiceRunner
@Component
public class BookServiceRunner implements ApplicationRunner {
@Autowired
BookService bookService;
@Override
public void run(ApplicationArguments args) throws Exception {
bookService.printBookRepository();
}
}
그럼 여러 개의 빈이 있을 때 모두 받아오는 방법은 없을까? 여러 빈을 모두 가져오고 싶을 때는 특정 어노테이션을 추가하지 않고 타입을 변경하면 된다. 예제에서는 List를 사용하도록 한다. 앱을 실행하면 위의 BookServiceRunner에서 MyBookRepository, OtherBookRepository가 모두 출력되는 것을 확인할 수 있다.
@Service
public class BookService {
@Autowired
List<BookRepository> bookRepository;
public void printBookRepository() {
this.bookRepository.forEach(System.out::println);
}
}
권장되는 방법은 아니지만 빈의 이름으로 설정하는 방법도 있다. 현재 빈으로 등록된 MyBookRepository와 OtherBookRepository 중 주입 받고 싶은 빈이 MyBookRepository라면 BookService에서 필드 이름을 카멜 케이스로 적어주면 이름을 보고 빈을 주입해준다. 이 방법은 BeanPostProcessor 인터페이스에 의해서 동작된다. 빈은 인스턴스가 만들어진 후에 초기화(Initialization) 라이프 사이클을 가지게 된다. 이 라이프 사이클 전이나 후에 작업을 할 수 있는 라이프 사이클 콜백이 있는데 그 때 작업을 할 수 있게 하는 게 BeanPostProcessor이다. BeanPostProcessor는 postProcessBeforeInitialization, postProcessAfterInitialization 메서드를 제공해 빈 초기화 전후에 작업을 할 수 있다. 이 BeanPostProcessor를 구현한 AutowiredAnnotationBeanPostProcessor가 @Autowired 어노테이션을 처리하면서 빈의 이름으로 의존성을 주입받을 수 있도록 해준다. 이 작업은 빈의 초기화 전에 진행된다. BeanPostProcessor와 AutowiredAnnotationBeanPostProcessor 모두 BeanFactory에 등록되어 있는 빈이다. 그래서 BeanFactory가 BeanPostProcessor 타입의 빈을 찾게 되고 그럼 AutowiredAnnotationBeanPostProcessor 또한 찾게 되어 다른 빈에 Autowired 어노테이션에 대한 로직이 적용된다.
@Service
public class BookService {
@Autowired
BookRepository myBookRepository;
public void printBookRepository() {
System.out.println(myBookRepository);
}
}