@Autowired에서 조회되는 빈이 2개 이상일 경우
- Spring/Spring
- 2021. 11. 12.
@Autowired에서 조회되는 빈이 2개 이상인 경우?
@Autowired는 의존관계 주입 단계에서 요구하는 객체에 대해 같은 타입을 가지는 빈을 스프링 컨테이너에서 조회한다. 이 때 조회된 빈이 1개면 문제가 없다. 2개 이상일 경우, getBean(Type) 검색에서 빈이 2개 이상 조회되었을 때와 동일한 에러가 발생한다. 먼저 아래에서 조회되는 빈이 2개 이상이 되도록 유발해본다.
@Component
public class FixDiscountPolicy implements DiscountPolicy
@Component
public class RateDiscountPolicy implements DiscountPolicy
위와 같이 Component를 설정하면, 스프링 컨테이너에는 DiscountPolicy 타입의 빈이 2개가 등록이 된다. 이걸 바탕으로 Test에 있는 BasicScan()을 실행해보면 NoUniqueBeanDefinition 에러가 발생한다. 에러 메세지를 끝까지 읽어보면 1개만 등록되어 있어야 하는데, RateDiscountPolicy, FixDiscount Policy가 등록되어있기 때문에 발생한다고 한다.
예를 들어 의존관계를 주입할 때, 필드 변수를 하위 구현체 타입으로 선언해서 이 문제를 해결할 수도 있다. 그렇지만 그 경우, 구체가 배우를 지정해야 하기 때문에 DIP 관점에서는 위반하게 된다. 그렇다면 이 문제를 어떻게 하면 해결할 수 있을까?
조회 빈 2개 이상일 때 해결방법
조회 빈이 2개 이상일 때 해결하는 방법은 아래처럼 세 가지 정도가 있다.
- Autowired 필드명 매칭
- @Qualifer → @Qualifier끼리 매칭(빈 이름 매칭)
- @Primary 사용
위의 세 가지 방법에 대해서 아래에서 좀 더 자세히 정리해보려고 한다.
@Autowired 필드명 매칭
Autowired는 기본적으로 타입으로 매칭을 한다. 타입으로 매칭했을 때, 조회되는 빈이 2개 이상이면 필드 이름과 파라미터 이름을 한번 더 매칭을 한다.
@Autowired
public OrderServiceImpl(DiscountPolicy discountPolicy, MemberRepository memberRepository) {
this.discountPolicy = discountPolicy;
this.memberRepository = memberRepository;
}
위의 코드에서 DiscountPolicy 의존관계 주입 시, 관련 빈이 2개가 조회된다.(RateDiscountPolicy, FixDiscountPolicy). Autowired는 타입으로 조회 했을 때, 조회된 것이 2개 이상이면 매개변수와 필드명을 매칭한다. 그렇지만 매개변수 discountPolicy가 스프링 컨테이너에는 조회되지 않기 때문에 문제가 해결되지 않는다.
@Autowired
public OrderServiceImpl(DiscountPolicy rateDiscountPolicy, MemberRepository memberRepository) {
this.discountPolicy = rateDiscountPolicy;
this.memberRepository = memberRepository;
}
이 때, 매개변수명을 의존관계 주입을 하고자 하는 빈 이름을 넣어주면 문제가 해결이 된다. 이렇게 하면 아래와 같이 동작한다
- @Autowired를 위해 DiscountPolicy 타입의 스프링 빈을 조회한다.
- 스프링 빈이 2개가 나왔기 때문에 매개변수 명과 동일한 스프링 빈을 선택해서 DI 해준다.
@Qualifier끼리 매칭하기
@Qualifer는 새로운 이름의 빈을 만드는 것이 아니라, 기존 빈에 추가적인 구분자를 달아주는 것으로 이해하면 된다. 추가적인 구분자가 필요없는 상황에서는 사용하지 않는다. Qualifer는 아래와 같이 동작한다
- 빈이 2개 이상 조회되었을 때, 같은 구분자를 가지는 @Qualifer 빈 객체를 찾아서 매칭해준다.
- 같은 구분자를 가지는 @Qualifier 객체가 없으면 스프링 빈 컨테이너에서 @Qualifer의 이름과 동일한 빈 객체를 찾아서 매칭해준다.
2번 같은 용도보다는 1번 같은 용도로만 사용하는 것으로 이해하고 사용하는 것이 좋다고 한다.
Qualifer 동작 테스트
@Autowired
public OrderServiceImpl(@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy,
MemberRepository memberRepository) {
this.discountPolicy = discountPolicy;
this.memberRepository = memberRepository;
}
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy
위의 코드처럼 @Qualifer를 달아주는 것으로 해결할 수 있다. @Qualifer 어노테이션 뒤에는 구분자를 입력할 수 있는데, 의존관계 주입 시 같은 구분자를 가지는 것을 찾는다. 예를 들어 위의 OrderServiceImpl에 @Qualifer("mainDiscountPolicy")가 되어있는데, 여기서 mainDiscountPolicy 구분자를 가지는 빈 객체를 찾지 못한다면, 그것와 동일한 이름을 가지는 빈 객체를 스프링 컨테이너에서 찾아서 매칭시켜주려고 한다.
만약에 이 때 그런 빈 객체를 찾지 못할 경우 "NoSucehBeanDefinitionException'이 발생하고, 에러메세지에는 "annotation.Qualifier(value="mainDiscountPolicy1")" 이런 내용이 뜬다.
@Primary 이용해서 매칭하기
@Primary는 우선순위를 정해서 매칭하는 방식이다. 예를 들어 빈 객체가 2개 이상 조회된다면, @Primary가 붙은 빈 객체가 우선적으로 의존관계 주입이 된다. 이런 특성 때문에 @Primary는 @Qualifier와 조합되어 많이 사용된다고 한다.
예를 들어 90%, 10%의 비율로 조회되는 DB들이 있다고 하면 90% 조회되는 DB는 @Primary로 달아주고, 10% 조회되는 특수한 DB는 @Qualifier 어노테이션을 활용해 구분자로 매칭시켜버리는 것이다.
@Primary 사용 예제
@Autowired
public OrderServiceImpl(DiscountPolicy discountPolicy,MemberRepository memberRepository) {
this.discountPolicy = discountPolicy;
this.memberRepository = memberRepository;
}
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy
@Component
public class FixDiscountPolicy implements DiscountPolicy
위의 코드를 실행하면 DiscountPolicy 타입 빈은 2개 조회된다. 여기서 @Primary 어노테이션이 붙은 RateDiscountPolicy가 우선적으로 의존관계 주입이 된다. 만약 특정 코드에서 FixDiscountPolicy를 쓰고 싶다면 @Qualifer를 활용해 구분자끼리 매칭시킬 수 있다.
@Primary / @Qualifer 같이 있다면?
스프링은 기본적으로 자동보다 수동이, 넓은 것보다는 좁은 것이, 포괄적인 것보다는 세부적인 것이 우선순위를 가진다. @Qualifer는 이름까지 정해서 매칭해주는 것이기 때문에 @Primary 대비 좁은 범위다. 따라서 @Qualifer가 우선순위를 가진다.
직접 겪은 예제1
설정 클래스
@Bean
@Qualifier("job1")
public Job jpaBatchItemWriterJob() {
return jobBuilderFactory
.get("jpaBatchItemWriterJob2")
.start(jpaBatchItemWriterStep())
.incrementer(new RunIdIncrementer())
.build();
}
@Bean
@Qualifier("job2")
public Job jdbcBatchItemWriterJob() {
return jobBuilderFactory
.get("jdbcBatchItemWriterJob2")
.start(jdbcBatchItemWriterStep())
.incrementer(new RunIdIncrementer())
.build();
}
생성자 주입 시, Qualifer 사용 → 성공
@Controller
public class TempController {
private final JobLauncher jobLauncher;
private final Job job1;
private final Job job2;
public TempController(JobLauncher jobLauncher, @Qualifier("job1") Job job1, @Qualifier("job2") Job job2) {
this.jobLauncher = jobLauncher;
this.job1 = job1;
this.job2 = job2;
}
생성자 주입 시, 빈 이름으로 매칭 → 성공
@Controller
public class TempController {
private final JobLauncher jobLauncher;
private final Job job1;
private final Job job2;
public TempController(JobLauncher jobLauncher, Job jpaBatchItemWriterJob, Job jdbcBatchItemWriterJob) {
this.jobLauncher = jobLauncher;
this.job1 = jpaBatchItemWriterJob;
this.job2 = jdbcBatchItemWriterJob;
}
@RequiredArgsConstructor 사용 시 → 안됨.
@ReuiqredArgsConstructor를 사용하면, 위에 사용했던 두 가지 방법(@Qualifier, 빈 이름 매칭)이 안되는 것으로 확인되었다. 혹시 나만 안되는 것일 수도 있는데.. 이 글을 보시는 분들 중 정확한 것을 알고 계시다면 알려주시면 감사하겠습니다. 이 글에 업데이트 하겠습니다.
'Spring > Spring' 카테고리의 다른 글
조회한 빈이 모두 필요할 때, List, Map (0) | 2021.11.12 |
---|---|
어노테이션 만들기 with @Qualifier (0) | 2021.11.12 |
스프링 생산성 향상, Lombok 라이브러리 사용 (0) | 2021.11.12 |
스프링, DI 시 Optional 처리하기 (0) | 2021.11.12 |
스프링, 다양한 DI 방법 (0) | 2021.11.12 |