본문 바로가기
Spring/스프링 핵심 원리 - 기본편

7-2. 의존 관계 주입 조회 빈이 2개 이상일 때

by hk27 2022. 1. 9.
Q. @Autowired를 하였는데, 조회되는 빈이 2개 이상이면 어떤 빈이 선택되나요?
A. 에러가 납니다. @Qualifier나 @Primary를 사용해주세요.

 

안녕하세요. 오늘은 의존 관계를 주입할 때, 조회되는 빈이 2개 이상일 때 어떻게 해결하는지 알아봅시다.

 

문제 상황

OrderServiceImpl 객체가 DiscountPolicy를 의존하고 있는 상황을 생각해봅시다. 

DiscountPolicy는 interface이고, 구현체로 FixDiscountPolicy와 RateDiscountPolicy가 있습니다.

정리하면 아래 사진과 같습니다.

OrederServiceImpl 객체가 DiscountPolicy 의존 관계를 주입 받으려고 할 때, FixDiscountPolicy 객체와 RateDiscountPolicy 객체가 모두 스프링 빈에 등록되어 있다면 무엇을 주입받을까요?

답은 '둘 다 받지 못하고, NoUniqueBeanDefinitionException 에러가 발생한다'입니다.

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

자세한 에러 메시지를 볼 수가 있습니다. 2개의 빈이 조회되었다고 합니다.

이런 경우 어떤 빈을 주입해야 할 지 알 수 없기 때문에, 에러가 발생합니다.

이 경우 2가지 방법으로 해결할 수 있습니다. @Qualifier와 @Primary입니다. 한 개씩 자세히 알아봅시다.

 

@Qualifier

@Qualifier는 추가 구분자를 붙여주는 방법입니다. 빈 이름을 변경하는 것이 아니라, 닉네임을 준 것입니다. (참고로 빈 이름은 클래스 명에서 제일 앞 알파벳을 소문자로 바꾼 것입니다! ex. FixDiscountPolicy -> fixDiscountPolicy)

빈을 등록할 때, 위에 '@Qualifier("원하는이름")'을 붙여주면 됩니다.

예시 코드를 살펴봅시다. 

빈 등록 예시

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

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

// 직접 빈에 등록할 때도 사용 가능합니다.
@Bean
@Qualifier("mainDiscountPolicy")
public DiscountPolicy discountPolicy() {return new RateDiscountPolicy();}

@Bean
@Qualifier("fixDiscountPolicy")
public DiscountPolicy fixDiscountPolicy() {return new FixDiscountPolicy();}

생성자 주입 예시

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

스프링은 @Qualifier를 보고, 맞는 객체를 찾아서 주입해줍니다. 위의 예시에서는 mainDiscountPolicy 구분자를 갖는 FixDiscountPolicy 객체가 주입됩니다.

그런데 이렇게 쓰면 빈을 등록할 때도, 의존관계를 주입받을 때도 @Qualifier를 달아줘야 하니까 번거롭습니다. 그리고 https://passionate.tistory.com/21에서 알아보았듯이 의존 관계를 주입 받는 클래스에 @RequiredArgsConstructor를 붙이면 생성자를 만들지 않아도 되는데, @Qualifier를 사용하려면 반드시 생성자를 만들어야 합니다. 이 문제를 해결할 수 있는 것이 @Primary입니다.

 

@Primary

Primary는 주요한 것, 1순위의 것이라는 의미가 있습니다. 여기서도 같은 의미로 사용됩니다. 의존 관계를 주입할 때 여러 빈이 매칭되면 @Primary가 우선권을 가집니다. 

예를 들어서, RateDiscountPolicy가 우선권을 가지게 하려면 아래 코드와 같이 사용하면 됩니다.

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}

@Component
public class FixDiscountPolicy implements DiscountPolicy {}

// 직접 빈에 등록할 때도 사용 가능합니다.
@Bean
@Primary
public DiscountPolicy rateDiscountPolicy() {return new RateDiscountPolicy();}

@Bean
public DiscountPolicy fixDiscountPolicy() {return new FixDiscountPolicy();}

주입 받는 코드에는 어노테이션을 달지 않아도 됩니다. 아래 코드처럼 사용하시면 됩니다. 

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

 

심지어는 생성자를 만들지 않고 @RequiredArgsConstructor를 사용해도 동작합니다. 매우 편하게 사용할 수 있습니다.

 

@Qualifier, @Primary  활용

그렇다면 언제는 @Qualifier를 쓰고, 언제는 @Primary를 사용할까요?

@Primary가 더 편리하기 때문에 기본적으로 @Primary를 사용하고, 서브로 @Qualifier를 사용하면 좋습니다.

예를 들어서, 코드에서 자주 사용하는 메인 DB 커넥션 획득 스프링 빈이 있고, 코드에서 가끔 사용하는 서브 DB 커넥션 획득 스프링 빈이 있다고 가정해봅시다.

메인 DB 커넥션 획득 빈에 @Primary를 적용해서 조회하는 곳에서 편리하게 이용하고,

서브 DB 커넥션 획득 빈에는 @Qualifier를 지정해서 조회하는 곳에서 @Qualifier로 받으면 코드를 깔끔하게 유지할 수 있습니다.

 

@Qualifier를 적용해도 @Primary가 우선적으로 적용되면 어떻게 하죠! 라는 의문점이 드실 수 있습니다.

그것은 걱정하지 않아도 됩니다. 

스프링은 언제나 자동보다는 수동이, 넓은 범위의 선택권보다는 좁은 범위의 선택권(자세한 것)이 우선순위가 높습니다. 여기서도 기본값처럼 동작하는 @Primary보다 상세하게 동작하는 @Qualifier의 우선권이 더 높습니다.

 

 

조회한 빈이 모두 필요할 때, Map, List

위에서는 여러 개의 빈 중에서 한 개를 선택하기 위해 @Qualifier와 @Primary가 사용된다는 것을 확인하였습니다.

그러나 가끔 해당 타입의 스프링 빈이 다 필요한 경우도 있겠죠.

예를 들어서, 할인 서비스를 제공하는데 클라이언트가 할인의 종류(rate, fix)를 선택할 수 있다면 우선 빈을 다 조회해야겠죠.

그때는 의존 관계를 주입받는 클래스에서 Map이나 List를 쓰면 됩니다.

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;
		System.out.println("policyMap = "+ policyMap);
		System.out.println("policies = "+ policies);
	}
}

이렇게 사용하면 DiscountPolicy로 조회한 빈이 모두 Map, List에 저장됩니다.

Map에는 key로 스프링 빈의 이름이 들어가고, value로 DiscountPolicy 스프링 빈 객체가 들어갑니다.

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

코드를 실행하면 출력 결과는 아래와 같습니다. 스프링 빈 2개가 잘 조회됨을 확인할 수 있습니다. 

policyMap = {rateDiscountPolicy=hello.core.discount.RateDiscountPolicy@5a7005d, fixDiscountPolicy=hello.core.discount.FixDiscountPolicy@5bc9ba1d}
policies = [hello.core.discount.RateDiscountPolicy@5a7005d, hello.core.discount.FixDiscountPolicy@5bc9ba1d]

 

인프런  '스프링 핵심 원리 - 기본편' 강의를 듣고 공부하며 정리한 자료입니다. 

잘못된 부분은 피드백 주시면 감사하겠습니다. 

글 읽어주셔서 감사합니다 :-)

 

참고자료

[1] 스프링 핵심 원리 - 기본편, 섹션 7. 의존관계 자동 주입 https://www.inflearn.com/course/스프링-핵심-원리-기본편

댓글