앞선 글에서 생성자 주입이 가장 권장되는 의존관계 주입 방식이라는 점을 살펴보았습니다.
실제로 대부분의 서비스 코드는 생성자 주입 또는 Lombok을 활용한 생성자 주입을 사용합니다.
그러나 생성자 주입을 사용하더라도, 타입은 맞는데 스프링이 빈을 주입하지 못하는 오류를 마주치는 경우가 있습니다.
이 글에서는 생성자 주입을 기준으로 왜 이러한 문제가 발생하는지, 스프링이 어떤 기준으로 빈을 선택하는지,
그리고 그 기준을 개발자가 어떻게 제어할 수 있는지를 설명합니다.
참고) 이 글에서 설명하는 빈 선택 규칙은 생성자 주입뿐만 아니라 필드 주입, setter 주입 등 모든 자동 의존관계 주입 방식에 동일하게 적용됩니다.
1. 문제 상황: 같은 타입의 빈이 2개일 때
구현체 두 개를 빈으로 등록한 경우
public interface DiscountPolicy {
int discount(Member member, int price);
}
@Component
public class FixDiscountPolicy implements DiscountPolicy { ... }
@Component
public class RateDiscountPolicy implements DiscountPolicy { ... }
이 상태에서 서비스 코드
@Service
@RequiredArgsConstructor
public class OrderServiceImpl {
private final DiscountPolicy discountPolicy;
}
이 코드를 실행하면 스프링은 애플리케이션 실행 시점에 오류를 발생시킵니다.
2. 스프링의 기본 빈 선택 기준은 타입
스프링의 자동 의존관계 주입은 기본적으로 타입(Type) 을 기준으로 동작합니다.
동작 흐름을 정리하면 다음과 같습니다.
- 생성자 파라미터의 타입을 확인합니다.
- 해당 타입의 빈을 컨테이너에서 모두 찾습니다.
- 결과가
- 1개라면 해당 빈을 주입합니다.
- 0개라면 빈이 없다는 오류가 발생합니다.
- 2개 이상이라면 어떤 빈을 선택해야 할지 알 수 없어 오류가 발생합니다.
위 예제에서는 DiscountPolicy 타입의 빈이 FixDiscountPolicy, RateDiscountPolicy 두 개 존재합니다.
이 경우 스프링은 둘 중 하나를 임의로 선택하지 않습니다. 이는 예측 불가능한 동작을 방지하기 위한 설계입니다.
즉, 이 오류는 스프링의 문제가 아니라 개발자의 의도가 코드에 충분히 드러나지 않았다는 신호입니다.
3. 해결 방법 ① 생성자 파라미터 이름으로 매칭
가장 단순한 해결 방법은 생성자 파라미터 이름을 빈 이름과 맞추는 것입니다.
@Service
@RequiredArgsConstructor
public class OrderServiceImpl {
private final DiscountPolicy rateDiscountPolicy;
}
이 경우 스프링은 다음과 같이 동작합니다.
- 타입으로 후보 빈들을 찾습니다.
- 후보가 여러 개라면 파라미터 이름과 동일한 빈 이름을 다시 찾습니다.
- 이름이 일치하는 빈이 있으면 해당 빈을 주입합니다.
이 방식의 특징은 다음과 같습니다.
- 설정이 간단합니다.
- 빠르게 문제를 해결할 수 있습니다.
다만, 파라미터명이 빈 이름에 강하게 의존하게 되며, 구현체가 늘어나거나 이름이 변경되면 유지보수가 어려워질 수 있습니다.
따라서 이 방법은 주로 개념 이해용 또는 임시 해결책으로 적합합니다.
5. 해결 방법 ② @Qualifier로 명시적으로 선택
@Qualifier는 같은 타입의 빈을 구분하기 위한 추가 식별자입니다.
빈 등록 시 @Qualifier를 지정
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy { }
생성자 주입 시 @Qualifier를 사용
@Service
@RequiredArgsConstructor
public class OrderServiceImpl {
@Qualifier("mainDiscountPolicy")
private final DiscountPolicy discountPolicy;
}
이 경우 스프링은 DiscountPolicy 타입의 빈 중에서 @Qualifier("mainDiscountPolicy")가 붙은 빈을 선택합니다.
이 방식은 다음과 같은 장점이 있습니다.
- 어떤 정책을 사용하는지 코드에 명확히 드러납니다.
- 이름 규칙에 과도하게 의존하지 않습니다.
여러 정책 구현체가 존재하는 상황에서는 실무에서 가장 많이 사용되는 방식이라고 합니다.
6. 해결 방법 ③ @Primary로 기본값을 지정합니다
@Primary는 여러 빈 중 기본으로 선택될 빈을 지정하는 방식입니다.
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy { }
이렇게 설정하면 다음과 같은 코드에서
private final DiscountPolicy discountPolicy;
별도의 추가 설정 없이 RateDiscountPolicy가 주입됩니다.
이 방식의 특징은 다음과 같습니다.
- 주입받는 쪽 코드가 매우 깔끔해집니다.
- 기본 정책이 명확할 때 적합합니다.
다만, 코드만 보고는 왜 이 구현체가 선택되었는지 바로 알기 어려울 수 있습니다.
7. @Qualifier와 @Primary의 선택 기준
두 방식은 목적이 다릅니다.
- @Primary는 기본값을 정하는 용도입니다.
- @Qualifier는 특정 구현을 명시적으로 선택하는 용도입니다.
일반적인 사용 기준은 다음과 같습니다.
- 대부분의 경우 공통으로 사용하는 기본 정책에는 @Primary를 사용합니다.
- 특정 상황에서만 사용하는 정책에는 @Qualifier를 사용합니다.
또한 중요한 규칙이 하나 있습니다.
@Qualifier는 @Primary보다 우선순위가 높습니다.
즉, 기본은 @Primary로 두고 필요한 경우에만 @Qualifier로 선택을 덮어쓸 수 있습니다.
'스프링 > 개념' 카테고리의 다른 글
| [빈 시리즈-2] 스프링은 언제 의존관계를 주입하는가 (0) | 2026.01.12 |
|---|---|
| [빈 시리즈-1] 스프링 빈은 어떻게 등록되는가 (1) | 2026.01.09 |
| 싱글톤 패턴의 한계와 싱글톤 컨테이너 (1) | 2026.01.08 |
| 커서 기반 페이징(Cursor-based Pagination) (0) | 2025.05.25 |