Published 2023. 5. 22. 02:08
들어가며
- 이전 포스트에서는 의존관계 자동주입의 방식 중 Constructor Injection(생성자 주입)을 사용해야 하는 여러가지 이유와 생성자나 Getter, Setter등과 같은 보일러플레이트 코드를 줄여주는 롬복을 알아보았다.
- 이번 포스트에서는 의존관계 자동 주입을 사용 시에 조회되는 스프링 빈이 2개 이상일 때 발생하는 문제와 이를 어떻게 해결할 수 있는지에 대한 방법을 알아본다.
조회된 스프링 빈이 2개 이상일 때 - 문제
- 다음의 코드를 보도록 하자.
// ApplicationRepository.interface
public interface ApplicationRepository{
...
}
// MemoryApplicationRepository.java
@Component
public class MemeoryApplicationRepository implements ApplicationRepository{
...
}
// JdbcApplicationRepository.java
@Component
public class JdbcApplicationRepository implements ApplicationRepository{
...
}
// ApplicationService.java
@Component
public class ApplicationService{
private final ApplicationRepository applicationRepository;
@Autowired
ApplicationService(ApplicationRepository applicationRepository){
this.applicationRepository = applicationRepository;
}
...
}
- @Autowired가 자동으로 의존 관계 주입을 하고 있다. 그렇다면 @Autowired는 어떻게 자동으로 의존 관계 주입을 수행하는가이다.
- 물론 스프링 DI 컨테이너에서 빈을 찾고 그것을 주입시킬 것이다. 즉 빈을 조회한 뒤에 찾은 빈을 주입시킨다.
- 이때 @Autowired는 타입(Type)을 통하여 스프링 빈을 조회한다.
- 즉 위의 코드는 ac.get(ApplicationRepository.class)로 스프링 빈을 가져온 뒤 그대로 주입하는 것과 동일한 방식이다.
- 그런데 이전 포스트 중 스프링 빈 조회를 본 적이 있다면 알겠지만 조회되는 빈이 2개 이상이라면 예외가 발생하며, 이를 해결해야 한다.
- 이때 발생하는 예외는 NoUniqueBeanDefinitionException 예외로 말 그대로 No Unique, 즉 유일하지 않은 빈 정의가 감지되었다는 것이다.
- 위 코드의 경우 ApplicationRepository.class의 하위 자식인 Memeory, Jdbc가 감지되어있기 때문에 위의 예외가 발생할 것이다.
조회된 스프링 빈이 2개 이상일 때 - 해결책
구체 클래스를 사용한 @Autowired 적용
- 가장 간단한 해결책은 부모 인터페이스 타입인 ApplicationRepository가 아닌 구현체 클래스를 지정하여 의존관계 주입을 수행하는 것이다.
- 코드로는 다음과 같다고 할 수 있겠다.
// ApplicationService.java
@Component
public class ApplicationService{
private final MemoryApplicationRepository memoryApplicationRepository;
@Autowired
ApplicationService(ApplicationRepository applicationRepository){
this.memoryApplicationRepository = memoryApplicationRepository;
}
...
}
- 그런데 잘 생각해보자. 우리가 이전에 DI를 사용했던 이유는 DIP와 OCP를 지키기 위한 발버둥의 결과였다.
- 이 방식을 사용하고 있을 때 만약 JdbcApplicationRepository로 사양이 변경되면 어떻게 될까?
- 당연히 구체적인 구현체를 명시하여 주입하고 있기 때문에 변경이 일어나야 할 것이다. 이는 OCP를 위배하게 된다.
- 이 이유는 이전에 DI에 대해 이해하고 있다면 금방 떠올릴 수 있을 것이다. 바로 DIP를 위배하고 있기 때문이다.
- DIP는 프로그래머는 항상 인터페이스와 같은 추상적인 것에 의존해야 한다고 하였다. 위의 코드는 그 구현체에 의존하고 있기 때문에 DIP가 지켜지지 않는 상태이다.
- 따라서 이러한 방법은 좋은 방법이 아니다.
@Autowired 필드 명
- @Autowired가 타입으로 빈을 매칭한다는 위에서 보고 왔다. 이때 @Autowired는 타입 매칭을 수행한 후, 여러 빈들이 감지되면 다음과 같은 방식으로 빈 이름을 추가적으로 매칭한다.
- @Autowired가 붙어 있는 필드의 이름
- @Autowired가 붙어 있는 메서드의 파라미터 이름
- 다음의 코드를 보자.
// ApplicationService.java
@Component
public class ApplicationService{
private final ApplicationRepository applicationRepository;
@Autowired
ApplicationService(ApplicationRepository memoryApplicationRepository){
this.applicationRepository = memoryApplicationRepository;
}
...
}
- @Autowired는 다음의 방식으로 동작하여 빈을 찾아 주입한다.
- 먼저 ApplicationRepository.class에 매칭되는 스프링 빈을 찾는다. MemoryApplicationRepository, JdbcApplicationRepository가 ApplicationRepository를 구현한 구현체이므로 2개의 빈이 찾아진다.
- 이후 @Autowired가 붙은 필드 이름을 찾는다. 현재 코드는 @Autowired가 붙은 필드 이름이 존재하지 않으므로 해당 단계를 스킵한다.
- @Autowired가 붙은 메서드의 파라미터 이름으로 매칭을 수행한다.
- 현재 메서드는 생성자로 파라미터 이름에 memoryApplicationRepository가 들어간다. 스프링 빈에 memoryApplicationRepository가 존재하기 때문에 매칭이 성공되어 해당 스프링 빈이 주입된다.
- @Autowired는 먼저 타입으로 빈을 탐색한 뒤 복수의 빈이 탐색되었을 때만 이후의 탐색 과정을 수행한다는 것에 유의하자.
@Qualifier 사용
- @Qualifier는 추가적인 구분자를 붙여주는 방식으로 위의 중복 빈 문제를 해결한다. 이때 유의해야 하는 점은 주입할 때 구분할 수 있는 추가적인 구분자를 붙여주는 것이지 빈의 이름을 변경하는 작업이 아니라는 것이다.
- 사용법은 간단한데 빈 등록 시 @Qualifier를 붙여주면 해결된다.
// ApplicationRepository.interface
public interface ApplicationRepository{
...
}
// MemoryApplicationRepository.java
@Component
@Qualifier("memoryApplicationRepository")
public class MemeoryApplicationRepository implements ApplicationRepository{
...
}
// JdbcApplicationRepository.java
@Component
@Qualifier("jdbcApplicationRepository")
public class JdbcApplicationRepository implements ApplicationRepository{
...
}
- 이후 DI 주입 시에 @Qualifier를 붙여주고 등록한 이름을 적어주면 해결된다.
// ApplicationService.java
@Component
public class ApplicationService{
private final ApplicationRepository applicationRepository;
@Autowired
ApplicationService(
@Qualifier("memoryApplicationRepository") ApplicationRepository applicationRepository
){
this.applicationRepository = applicationRepository;
}
...
}
- 만약 @Qualifier를 사용하여 DI를 수행할 때 매칭되는 스프링 빈이 존재하지 않는다면 NoSuchBeanDefinitionException을 발생시킨다.
@Primary 사용
- @Primary는 @Qualifier와는 다르게 동작하는데 @Autowired로 탐색 시에 여러 빈이 매칭되면 @Primary 어노테이션이 붙은 스프링 빈이 우선권을 가진다. 즉 먼저 매칭되어 주입된다.
// ApplicationRepository.interface
public interface ApplicationRepository{
...
}
// MemoryApplicationRepository.java
@Component
@Primary
public class MemeoryApplicationRepository implements ApplicationRepository{
...
}
// JdbcApplicationRepository.java
@Component
public class JdbcApplicationRepository implements ApplicationRepository{
...
}
- 우선권을 가질 스프링 빈에 @Primary 어노테이션을 붙여 준다.
// ApplicationService.java
@Component
public class ApplicationService{
private final ApplicationRepository applicationRepository;
@Autowired
ApplicationService(
ApplicationRepository applicationRepository
){
this.applicationRepository = applicationRepository;
}
...
}
- 이후에는 그냥 이전에 사용한 생성자 방식을 사용하면 된다. 이렇게 되면 @Autowired로 인해 여러 빈이 매칭 시 @Primary 어노테이션이 붙은 memoryApplicationRepository가 선택되어 주입된다.
@Qualifier vs @Primary
- 두 어노테이션을 사용하여 스프링 빈이 중복으로 탐색될 때에 대한 문제를 해결할 수 있었다. 그런데 두 어노테이션을 살펴보면 약간의 차이가 있는데 어떤 방식을 사용하는게 좋은지에 대해 고민할 필요가 있다.
- @Qualifier는 사용 시 명확하게 구분자를 통해 식별을 할 수 있게 해주지만, DI가 필요한 모든 곳과 2개 이상의 빈이 탐색될 모든 빈에 @Qualifier로 구분자를 명시해주어야 하는 단점이 있다.
- @Primary는 우선 순위를 제시하여 클라이언트 코드에 변화가 없게 할 수 있으나 만약 여러 개의 빈들이 있을 때 이 모든 빈들의 우선순위들을 세밀하게 제어할 수 있는지에 대해 의문이 생긴다.
- @Primary는 다음과 같이 사용할 때 유용할 수 있다.
- Main과 Sub가 나뉘어 사용되는 스프링 빈들이 존재할 때
- 만약 메인 데이터베이스와 서브 데이터베이스가 존재할 때 각 데이터베이스의 커넥션을 가져오는 빈이 존재한다고 가정하자. 메인 데이터베이스는 서브와 비교하여 더 많이 사용될 것이고, DI에서도 더 많이 주입될 것이다.
- 따라서 메인 데이터베이스의 커넥션을 가져오는 스프링 빈에 @Primary를 통해 코드의 변화를 줄이고, 서브 데이터베이스 커넥션을 가져오는 스프링 빈을 가져올 때는 @Qualifier를 통해 가져올 수 있을 것이다.
- 이런 방식을 사용하면 메인 데이터베이스 커넥션 스프링 빈을 주입할 때는 코드의 변경이 없게 할 수 있고, 서브 데이터베이스 커넥션 스프링 빈을 주입할 때도 명시적으로 해줌으로서 깔끔하게 DI를 수행할 수 있다.
@Qualifier 어노테이션 직접 만들기
- @Qualifier를 사용하면 명시적인 정의를 통해 빈을 찾을 수 있지만 컴파일 시 타입 체크가 안된다는 단점이 있다.
- 이를 어노테이션을 직접 만듬으로서 해결할 수 있다.
// MainApplicationRepository.annotaion
@Target({
ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER,
ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}
- 이렇게 만든 MainDiscountPolicy 어노테이션을 스프링 빈에 추가시키면 된다.
// ApplicationRepository.interface
public interface ApplicationRepository{
...
}
// MemoryApplicationRepository.java
@Component
@MainApplicationRepository
public class MemeoryApplicationRepository implements ApplicationRepository{
...
}
// JdbcApplicationRepository.java
@Component
public class JdbcApplicationRepository implements ApplicationRepository{
...
}
- 이후 DI를 수행할 때 파라미터에 해당 어노테이션을 추가하여 수행할 수 있다.
// ApplicationService.java
@Component
public class ApplicationService{
private final ApplicationRepository applicationRepository;
@Autowired
ApplicationService(
@MainApplicationRepository ApplicationRepository applicationRepository
){
this.applicationRepository = applicationRepository;
}
...
}
정리
- @Autowired가 타입을 통해 빈을 매칭하며 매칭된 빈이 2개 이상이면 발생하는 문제와 이를 해결하기 위해 생긴 여러가지 방법들을 살펴보았다.
- 다음 포스트에서는 전체적인 자동 의존관계 주입에 대해 정리해보고, 자동 빈 등록과 수동 빈 등록을 사용하는 기준과 판단에 대해 살펴본다.
'Spring & JPA > Spring' 카테고리의 다른 글
Spring Framework - 스프링 빈 생명주기와 콜백 (0) | 2023.05.24 |
---|---|
Spring Framework - 의존관계 자동 주입 - 4. 정리 (0) | 2023.05.23 |
Spring Framework - 의존관계 자동 주입 - 2. 생성자 주입의 장점과 lombok (0) | 2023.05.18 |
Spring Framework - 의존관계 자동 주입 - 1. 의존관계 주입 방법 (1) | 2023.05.17 |
Spring Framework - 컴포넌트 스캔 - 2. 컴포넌트 스캔 위치, 대상, 필터 (0) | 2023.05.15 |