(Spring) @Autowired 사용 시 NoUniqueBeanDefinitionException 에러 해결방법
@Autowired를 사용하다 보면
NoUniqueBeanDefinitionException: No qualifying bean of type
에러를 심심치 않게 마주할 수 있다.
이번 포스팅에서는 NoUniqueBeanDefinitionException에러가 무엇이고 이를 해결하는 방법들에 대해서 알아보고자 한다.
에러 발생 원인
NoUniqueBeanDefinitionException 에러는 의존성을 자동 주입할 때, 주입하려는 빈이 하나가 아닌 2개 이상 존재하여 어떤 빈을 주입해야 하는지 알 수 업을 때 발생하는 에러이다.
예를 들어, 할인 정책의 기능을 수행하는 객체인 DiscountPolicy라는 인터페이스가 있고, 고정 금액 할인, 고정 비율 할인을 실제로 적용하는 FixDiscountPolicy, RateDiscountPolicy구현체가 있을 때 두 구현체를 빈으로 등록한다면 아래와 같다.
@Component
public class FixDiscountPolicy implements DiscountPolicy {
// 내용
}
@Component
public class RateDiscountPolicy implements DiscountPolicy {
// 내용
}
이 때, 주문을 처리하는 OrderServiceImpl에서 아래와 같이 DiscountPolicy에 @Autowired를 통해 의존성 주입을 시행하면
아래와 같이 NoUniqueBeanDefinitionException 에러가 발생한다.
@Autowired
private DiscountPolicy discountPolicy
NoUniqueBeanDefinitionException: No qualifying bean of type
'hello.core.discount.DiscountPolicy' available: expected single matching bean
but found 2: fixDiscountPolicy,rateDiscountPolicy
DiscountPolicy에 자동으로 의존성 주입을 실행하려 하는데 FixDiscountPolicy와 RateDiscountPolicy 두 개의 빈이 같은 타입이기 때문에 어떤 빈을 주입해야 할지 모른다는 것이다.
해결방법
사실 아주 간단한 해결 방법이 하나 있다.
DiscountPolicy 타입을 구체화하여 RateDiscountPolicy나 FixDiscountPolicy로 지정해주면 된다. 바로 아래와 같이.
@Autowired
private RateDiscountPolicy discountPolicy
OR
@Autowired
private FixDiscountPolicy discountPolicy
그렇지만 이는 좋은 해결 방법이 아니다. SOLID 원칙의 DIP를 위반하기 때문이다. DIP는 구현체가 아닌 추상 클래스나 인터페이스를 의존해야 한다는 원칙이다. (SOLID가 뭔지 모른다면? 여기로)
그러므로 우리는 다른 해결 방법을 찾아야 한다. 해결 방법에는 대표적으로 3가지 방법이 있다.
1. @Autowired 필드 명
Autowired를 사용할 때 타입이 일치하는 2개 이상의 빈이 존재한다면 스프링은 자동적으로 필드명을 비교한다. 필드명은 RateDiscountPolicy -> rateDiscountPolicy
FixDiscountPolicy -> fixDiscountPolicy
의 형태로 빈이 생성될때 부여되는 이름이다.
따라서, 아래와 같은 형태로 사용한다면 타입 중복의 문제를 피할 수 있다.
참고 : 필드 명 매칭은 먼저 타입 매칭을 시도 하고 그 결과에 여러 빈이 있을 때 추가로 동작하는 기능이다
@Autowired
private DiscountPolicy rateDiscountPolicy
OR
@Autowired
private DiscountPolicy fixDiscountPolicy
2. @Qualifier
두번째 방법으로는 Qualifer어노테이션을 사용하는 것이다. 이는 추가 구분자를 통해 타입이 같아도 다른 빈으로 구별할 수 있도록 동작한다.
컴포넌트로 빈을 등록할 때 @Qualifier 어노테이션과 이름을 사용하여 지정할 수 있다.
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {
// 내용
}
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {
// 내용
}
@Autowired에선 아래와 같이 사용하면 된다.
@Autowired
private @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy
OR
@Autowired
private @Qualifier("fixDiscountPolicy") DiscountPolicy discountPolicy
만약 설정한 Qualifier인 mainDiscountPolicy가 없으면 어떻게 될까?
스프링은 자동적으로 "mainDiscountPolicy"라는 필드명을 가진 스프링 빈을 검색한다.
그러나 @Qualifier는 Qualifier를 찾는 용도로만 사용하는 것이 좋다고 한다.
또한, 필드명도 찾지 못한다면 NoSuchBeanDefinitionException에러가 발생한다.
@Qualifier 의 단점은 주입 받을 때 다음과 같이 모든 코드에 @Qualifier 를 붙여주어야 한다는 점이다.
참고 : @ComponentScan을 사용하지 않고 직접 Bean 등록시에도 똑같이 사용 가능하다.
3. @Primary
@Primary 는 우선순위를 정하는 방법이다. @Autowired 시에 여러 빈이 매칭되면 @Primary 가 우선권을
가진다.
@Primary 를 사용하면 @Qualifier 를 붙일 필요가 없다.
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {
// 내용
}
@Component
public class FixDiscountPolicy implements DiscountPolicy {
// 내용
}
스프링 빈을 생성할 때 위와 같이 @Primary 어노테이션을 붙인다면 NoUniqueBeanDefinitionException가 발생했을 때
자동적으로 RateDiscountPolicy를 우선적으로 주입하게 된다.
@Autowired
private DiscountPolicy discountPolicy
// 자동적으로 @Primary인 RateDiscountPolicy가 주입됨
우선순위
@Primary 는 기본값 처럼 동작하는 것이고, @Qualifier 는 매우 상세하게 동작한다. 이런 경우 어떤 것이
우선권을 가져갈까? 스프링은 자동보다는 수동이, 넒은 범위의 선택권 보다는 좁은 범위의 선택권이 우선
순위가 높다. 따라서 여기서도 @Qualifier 가 우선권이 높다.
*본 포스팅은 김영한님의 "스프링 핵심 원리 - 기본편" 에서 학습한 내용을 기반으로 작성되었습니다.