[Spring] Spring 핵심 원리 : 같은 타입의 빈을 2개 이상 등록

빈이 두 개 이상 조회된다면?
@Autowired는 타입으로 조회한다. applicationContext.getBean(클래스명.class)와 유사한 방식으로 동작한다. 그런데 아래 코드와 같이 같은 타입의 빈이 두 개 이상 등록된 경우 자동 의존관계 주입이 실행되면 NoUniqueBeanDefinitionException 예외가 발생한다.
@Component
public class FixDiscountPolicy implements DiscountPolicy {}
@Component
public class RateDiscountPolicy implements DiscountPolicy {}
@Autowired
public OrderServiceImpl(OrderRepository orderRepository, MemberRepository memberRepository, DiscountPolicy DiscountPolicy) {
this.orderRepository = orderRepository;
this.memberRepository = memberRepository;
this.DiscountPolicy = DiscountPolicy;
}
의존관계를 구체 타입으로 지정하면 해결되겠지만, 이런 경우 DIP를 위반하고 유연성이 떨어진다. 또는 수동 빈 등록으로 해결할 수 있지만 의존관계 자동 주입에서 해결하는 방법이 있다.
@Autowired의 필드명 매칭
@Autowired는 먼저 타입으로 매칭을 시도한다. 그 후, 중복되는 빈이 여러 개 있으면 필드 이름 혹은 매개변수 이름으로 매칭을 시도한다. 그래서 @Autowired 기능 자체로 빈 중복 문제를 해결하고 싶으면 다음과 같이 코드를 작성하면 된다.
@Autowired
public OrderServiceImpl(OrderRepository orderRepository, MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) {
this.orderRepository = orderRepository;
this.memberRepository = memberRepository;
this.DiscountPolicy = rateDiscountPolicy;
}
- @Autowired 실행 과정
- 타입 매칭
- 1의 결과가 두 개 이상인 경우, 필드 명 혹은 매개변수 명으로 매칭
@Qualifier
@Qualifier는 추가 구분자를 붙여주는 방법이다. 빈 이름을 변경하는 것이 아니다.
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {}
@Autowired
public OrderServiceImpl(OrderRepository orderRepository,
MemberRepository memberRepository,
@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
this.orderRepository = orderRepository;
this.memberRepository = memberRepository;
this.DiscountPolicy = rateDiscountPolicy;
}
@Qualifier("mainDiscountPolicy")를 찾지 못하면 mainDiscountPolicy라는 이름의 빈을 추가로 찾는다.
- @Qualifier 실행 과정
- @Qualifier끼리 매칭
- 1에서 매칭에 실패한 경우, @Qualifier에 설정한 이름으로 매칭
@Primary
@Primary는 우선 순위를 정하는 방법이다. 자동 의존관계 주입 시 여러 빈이 매치되면 @Primary가 붙은 스프링 빈이 우선권을 갖는다.
@Component
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
public class FixDiscountPolicy implements DiscountPolicy {}
@Autowired
public OrderServiceImpl(OrderRepository orderRepository,
MemberRepository memberRepository,
DiscountPolicy discountPolicy) {
this.orderRepository = orderRepository;
this.memberRepository = memberRepository;
this.DiscountPolicy = rateDiscountPolicy;
}
@Qualifier vs @Primary
@Qualifier와 @Primary는 기능이 비슷해 보인다. 다만 @Qualifier의 단점은 식별하고자 하는 빈을 주입받는 모든 필드에 @Qualifier 애노테이션을 붙여야 한다는 것이다. 반면, @Primary는 우선권을 부여할 빈에 @Primary 애노테이션만 붙이면 된다. 그것을 주입받는 필드에는 붙일 필요가 없다. 그럼 두 애노테이션의 우선순위는 어떻게 결정될까?
@Primary는 기본값 처럼 동작하고 @Qualifier는 매우 상세하기 동작한다. 스프링은 자동보다는 수동이, 넓은 범위보다는 좁은 범위의 선택권이 우선순위가 높다. 그래서 @Qualifier가 @Primary보다 우선순위가 높다.
위와 같은 방법들은 DIP, OCP 위반 아닌가?
빈 중복 조회를 피하기 위해 @Autowired를 사용하면 의존 객체를 주입받는 구체 클래스의 매개변수 이름 혹은 필드 이름을 수정해야 한다. @Primary를 사용하면 우선권을 부여할 빈의 구현 클래스에 애노테이션을 붙여야 한다. 그리고 @Qualifier를 사용하면 의존 관계에 있는 두 구체 클래스 모두에 애노테이션을 붙여야 한다.
이런 경우 의존 관계를 변경해야 할 때, 설정 클래스가 아닌 애플리케이션의 기능을 수행하는 클래스를 직접 수정해야 한다. 이 상황은 의존성 역전 원칙(DIP)과 개방-폐쇄 원칙(OCP)을 위반한 것 같다. 맞는 말이지만 이것은 컴포넌트 스캔의 한계다. @Bean 애노테이션을 사용해서 빈을 직접 등록하면 위반하지 않을 수 있지만 불편하다. 따라서 둘 의 트레이드오프 관계라고 이해하면 된다.