프레임워크(Framework)/Spring

[Spring] 스프링 조회한 빈(Bean)이 2개 이상인 경우 문제 해결 방법

잇트루 2022. 11. 7. 00:49
반응형
본 내용은 온라인 강의 사이트 인프런의 김영한 님의 강의 내용이 포함되어 있습니다.
'스프링 핵심 원리 - 기본편'
 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

 

Intro

@Autowired는 의존성 주입을 위해 생성자 파라미터를 타입으로 조회하기 때문에 빈이 2개 이상 조회될 수 있다.

예를 들어, DiscountPolicy라는 할인 정책 인터페이스가 있으며, 구현체는 고정 할인 정책과 정률 할인 정책인 FixDiscountPolicy, RateDiscountPolicy가 있다.

@Component
public class FixDiscountPolicy implements DiscountPolicy {
    ...
}

@Component
public class RateDiscountPolicy implements DiscountPolicy {
    ...
}
@Component
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
        ...
}

@Component를 통해 둘 다 스프링 빈으로 등록하고 @Autowired로 의존관계 자동 주입하여 실행하면 NoUniqueBeanDefinitionException 예외가 발생한다.

NoUniqueBeanDefinitionException: No qualifying bean of type 'hello.core.discount.DiscountPolicy' available: expected single matching bean but found 2: fixDiscountPolicy,rateDiscountPolicy

NoUniqueBeanDefinitionException 예외는 1개의 빈을 기대하였으나, fixDiscountPolicy, rateDiscountPolicy 2개가 발견되어 발생한 예외이다.

 

이때 하위 타입으로 지정할 수도 있지만, 하위 타입으로 지정하는 것은 DIP를 위배하고 유연성이 떨어진다.

또한, 이름만 다르고 똑같은 타입의 스프링 빈이 2개 있을 때 해결이 되지 않는다.

스프링 빈을 수동 등록하여 문제를 해결할 수도 있지만, 의존 관계 자동 주입을 통해 해결하는 여러 가지 방법이 있다.

 

@Autowired 필드 명 매칭

@Autowired은 타입 매칭을 시도한다. 이때 여러 개의 빈이 있으면 필드 이름, 파라미터 이름으로 빈 이름을 추가한다.

따라서 의존 관계 주입 코드에서 필드 명을 빈 이름으로 변경하는 방법이 있다.

 

필드 명을 빈 이름으로 변경

@Component
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    @Autowired
    private DiscountPolicy rateDiscountPolicy;

    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy rateDiscountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = rateDiscountPolicy;
    }
		...
}

이때, 필드 명이 rateDiscountPolicy이므로 정상 주입이 된다.

필드 명 매칭은 먼저 타입 매칭을 시도하고, 그 결과에 여러 빈이 있을 때 추가로 동작하는 기능이다.

 

@Qualifier 사용

@Qualifier는 추가 구분자를 붙여주는 방법이다.

@Qualifier는 추가 구분자에 해당하는 빈을 찾고, 찾지 못하면 스프링 빈 이름으로 찾게 된다.

이는 의존관계 주입 시 추가적인 방법을 제공하는 것으로 빈 이름을 변경하는 것은 아니다.

다음과 같이 스프링 빈에 등록할 대상에 @Qualifier 어노테이션을 붙여 구분자 설정을 한다.

@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {
		...
}

@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {
		...
}

 

이후 주입 대상에 사용할 빈의 추가 구분자 @Qualifier를 사용한다.

@Component
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository,
                        @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
        ...
}

이때 만약, @Qualifier("mainDiscountPolicy")를 못 찾으면 mainDiscountPolicy라는 이름의 스프링 빈을 추가로 찾게 된다. 스프링 빈을 추가로 찾지 못하면 NoSuchBeanDefinitionException이 발생한다.

 

@Qualifier은 수동 빈 등록 시에도 사용할 수 있다.

@Bean
@Qualifier("mainDiscountPolicy")
public DiscountPolicy discountPolicy() {
		...
}

 

@Primary 사용

@Primary는 우선순위를 정하는 방법이다.

@Autowired에서 여러 개의 빈이 매칭 되면 @Primary가 붙어 있는 빈이 우선권을 가지는 방법이다.

@Component
public class FixDiscountPolicy implements DiscountPolicy {
		...
}

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {
		...
}

@Primary는 가장 많이 사용되고 있는 방법이기도 하다.

만약, @Qualifier과 @Primary를 동시에 사용한다면, @Qualifier가 우선권을 가진다.

 

List, Map 활용하기

만약, 조회한 빈이 2개 이상일 때, 우선권을 부여하여 선택하는 것이 아닌 모두 활용해야 하는 경우가 존재할 수 있다.

위 예제에서 rateDiscountPolicy와 fixDiscountPolicy를 모두 사용해야 하는 경우가 될 수 있다.

public class DiscountService {
    private final Map<String, DiscountPolicy> policyMap;
    private final List<DiscountPolicy> policies;

    @Autowired
    public DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies) {
        this.policyMap = policyMap;
        this.policies = policies;
    }

    public int discount(Member member, int price, String discountCode) {
        DiscountPolicy discountPolicy = policyMap.get(discountCode);
        return discountPolicy.discount(member, price);
    }
}

위 코드와 같이 Map과 List에 의존성 주입을 받을 수 있다. Map에 fixDiscountPolicy와 rateDiscountPolicy가 주입된다.

discount() 메서드를 통해 discountCode로 Map에서 “fixdiDiscountPolicy” 또는 “rateDiscountPolicy”가 넘어오면서 스프링 빈을 찾아서 실행할 수 있다.

 

Map에는 Key 값에 스프링 빈의 이름을 넣고, 값으로 DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담는다.

List에는 DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담는다.

만약, 해당하는 타입의 스프링 빈이 없으면, 빈 컬렉션이나 Map을 주입하게 된다.

반응형