[Spring] 10. @Qualifier와 @Primary
이전 글에서는 @Autowired와 생성자 주입에 대해 살펴봤습니다. Spring은 Container 안에 등록된 Bean을 찾아 필요한 곳에 자동으로 주입해줍니다.
하지만 항상 자동 주입이 쉽게 성공하는 것은 아닙니다. 특히 같은 타입의 Bean이 여러 개 등록되어 있으면 Spring은 어떤 Bean을 주입해야 할지 판단하지 못할 수 있습니다.
이런 상황에서 사용하는 대표적인 방법이 @Qualifier와 @Primary입니다.
1. 같은 타입의 Bean이 여러 개 있을 때 생기는 문제
먼저 결제 기능을 예로 들어보겠습니다. 결제 기능을 처리하기 위해 PayService라는 인터페이스가 있다고 가정합니다.
public interface PayService {
void pay();
}
그리고 이 인터페이스를 구현한 클래스가 두 개 있다고 해보겠습니다.
@Service
public class KakaoPayService implements PayService {
@Override
public void pay() {
System.out.println("카카오페이 결제");
}
}
@Service
public class NaverPayService implements PayService {
@Override
public void pay() {
System.out.println("네이버페이 결제");
}
}
두 클래스 모두 @Service가 붙어 있으므로 Spring Bean으로 등록될 수 있습니다. 문제는 둘 다 PayService 타입이라는 점입니다.
PayService 타입 Bean
├─ kakaoPayService
└─ naverPayService
이 상태에서 OrderService가 PayService를 주입받으려고 하면 문제가 생길 수 있습니다.
@Service
public class OrderService {
private final PayService payService;
public OrderService(PayService payService) {
this.payService = payService;
}
}
Spring 입장에서는 PayService 타입의 Bean이 두 개 있습니다. 그래서 KakaoPayService를 넣어야 할지, NaverPayService를 넣어야 할지 애매해집니다.
Spring Container
→ PayService 타입 Bean 찾기
→ kakaoPayService 발견
→ naverPayService 발견
→ 어떤 Bean을 넣어야 할지 모름
이런 상황을 해결하기 위해 @Qualifier나 @Primary를 사용할 수 있습니다.
2. @Qualifier란?
@Qualifier는 여러 Bean 중에서 어떤 Bean을 주입할지 이름으로 지정할 때 사용합니다.
예를 들어 OrderService에서 KakaoPayService를 사용하고 싶다면 생성자 파라미터에 @Qualifier를 붙일 수 있습니다.
@Service
public class OrderService {
private final PayService payService;
public OrderService(@Qualifier("kakaoPayService") PayService payService) {
this.payService = payService;
}
}
이 코드는 PayService 타입 Bean 중에서 이름이 kakaoPayService인 Bean을 주입하라는 의미입니다.
@Qualifier("kakaoPayService")
→ kakaoPayService Bean을 선택
Spring은 기본적으로 클래스 이름의 첫 글자를 소문자로 바꾼 이름을 Bean 이름으로 사용합니다. 그래서 KakaoPayService는 보통 kakaoPayService라는 이름의 Bean으로 등록됩니다.
3. Bean 이름 확인하기
Bean 이름은 기본적으로 클래스 이름을 기준으로 만들어집니다.
| 클래스 이름 | 기본 Bean 이름 | 설명 |
| KakaoPayService | kakaoPayService | 첫 글자 소문자 |
| NaverPayService | naverPayService | 첫 글자 소문자 |
| BoardService | boardService | 첫 글자 소문자 |
따라서 @Qualifier를 사용할 때는 Bean 이름을 정확히 작성해야 합니다.
public OrderService(@Qualifier("naverPayService") PayService payService) {
this.payService = payService;
}
위 코드는 NaverPayService Bean을 주입받는 예시입니다.
4. Bean 이름을 직접 지정하기
Bean 이름은 직접 지정할 수도 있습니다. @Service에 이름을 넣으면 해당 이름으로 Bean이 등록됩니다.
@Service("kakaoPay")
public class KakaoPayService implements PayService {
@Override
public void pay() {
System.out.println("카카오페이 결제");
}
}
이 경우 Bean 이름은 kakaoPayService가 아니라 kakaoPay가 됩니다.
따라서 주입할 때도 지정한 이름을 사용해야 합니다.
@Service
public class OrderService {
private final PayService payService;
public OrderService(@Qualifier("kakaoPay") PayService payService) {
this.payService = payService;
}
}
직접 이름을 지정하면 의미를 더 분명하게 만들 수 있습니다. 다만 이름을 잘못 작성하면 Bean을 찾지 못하므로 주의해야 합니다.
5. @Primary란?
@Primary는 같은 타입의 Bean이 여러 개 있을 때 기본으로 사용할 Bean을 지정하는 어노테이션입니다.
예를 들어 결제 방식 중에서 기본 결제를 카카오페이로 사용하고 싶다면 KakaoPayService에 @Primary를 붙일 수 있습니다.
@Primary
@Service
public class KakaoPayService implements PayService {
@Override
public void pay() {
System.out.println("카카오페이 결제");
}
}
@Service
public class NaverPayService implements PayService {
@Override
public void pay() {
System.out.println("네이버페이 결제");
}
}
이 상태에서 OrderService가 PayService를 주입받으면 Spring은 기본적으로 @Primary가 붙은 KakaoPayService를 선택합니다.
@Service
public class OrderService {
private final PayService payService;
public OrderService(PayService payService) {
this.payService = payService;
}
}
PayService 타입 Bean 여러 개 발견
→ @Primary 확인
→ KakaoPayService 선택
@Primary를 사용하면 주입받는 쪽 코드에 @Qualifier를 붙이지 않아도 됩니다. 기본 Bean을 하나 정해두는 방식이라고 생각하면 됩니다.
6. @Qualifier와 @Primary의 차이
@Qualifier와 @Primary는 둘 다 여러 Bean 중 하나를 선택할 때 사용합니다. 하지만 사용 방식이 다릅니다.
| 구분 | @Qualifier | @Primary |
| 의미 | 주입할 Bean을 이름으로 직접 지정합니다. | 기본으로 사용할 Bean을 지정합니다. |
| 사용 위치 | 주입받는 생성자, 필드, 파라미터 등에 사용합니다. | Bean으로 등록되는 클래스에 사용합니다. |
| 선택 방식 | 명확하게 특정 Bean을 선택합니다. | 기본 후보를 정합니다. |
| 적합한 상황 | 상황별로 다른 Bean을 골라야 할 때 | 대부분 한 Bean을 기본으로 사용할 때 |
정리하면 @Primary는 기본값을 정하는 방식이고, @Qualifier는 직접 콕 집어서 선택하는 방식입니다.
@Primary
→ 기본 Bean 선택
@Qualifier
→ 특정 Bean 직접 선택
7. @Qualifier가 @Primary보다 우선한다
만약 @Primary와 @Qualifier를 함께 사용하면 어떻게 될까요?
일반적으로 @Qualifier가 더 구체적인 지정이기 때문에 우선합니다.
@Primary
@Service
public class KakaoPayService implements PayService {
}
@Service
public class NaverPayService implements PayService {
}
@Service
public class OrderService {
private final PayService payService;
public OrderService(@Qualifier("naverPayService") PayService payService) {
this.payService = payService;
}
}
KakaoPayService에 @Primary가 붙어 있지만, OrderService에서는 @Qualifier("naverPayService")를 사용했습니다. 이 경우 Spring은 NaverPayService를 주입합니다.
@Primary는 기본 선택
@Qualifier는 직접 지정
직접 지정한 @Qualifier가 더 우선
8. 실전에서는 어떻게 선택할까?
실전에서는 상황에 따라 선택하면 됩니다.
| 상황 | 추천 방식 |
| 대부분 하나의 구현체를 기본으로 사용 | @Primary |
| 특정 위치에서만 다른 구현체 사용 | @Qualifier |
| 구현체가 여러 개이고 상황별로 명확히 선택해야 함 | @Qualifier |
| 기본 구현체도 있고 예외적인 구현체도 있음 | @Primary + @Qualifier |
예를 들어 대부분의 결제는 카카오페이를 사용하고, 특정 이벤트 결제에서만 네이버페이를 사용한다면 카카오페이에 @Primary를 붙이고, 이벤트 결제 쪽에서는 @Qualifier로 네이버페이를 지정할 수 있습니다.
9. 게시판 프로젝트 예시
게시판 프로젝트에서도 비슷한 상황이 생길 수 있습니다. 예를 들어 파일 저장 방식을 나눈다고 생각해보겠습니다.
public interface FileStorageService {
void saveFile();
}
로컬 서버에 저장하는 방식과 클라우드에 저장하는 방식이 있을 수 있습니다.
@Primary
@Service
public class LocalFileStorageService implements FileStorageService {
@Override
public void saveFile() {
System.out.println("로컬 파일 저장");
}
}
@Service
public class CloudFileStorageService implements FileStorageService {
@Override
public void saveFile() {
System.out.println("클라우드 파일 저장");
}
}
기본적으로는 로컬 저장 방식을 사용하고 싶다면 LocalFileStorageService에 @Primary를 붙일 수 있습니다.
@Service
public class BoardFileService {
private final FileStorageService fileStorageService;
public BoardFileService(FileStorageService fileStorageService) {
this.fileStorageService = fileStorageService;
}
}
이 경우 BoardFileService에는 기본 Bean인 LocalFileStorageService가 주입됩니다.
반대로 특정 기능에서 클라우드 저장 방식을 사용하고 싶다면 @Qualifier를 사용할 수 있습니다.
@Service
public class CloudUploadService {
private final FileStorageService fileStorageService;
public CloudUploadService(
@Qualifier("cloudFileStorageService") FileStorageService fileStorageService
) {
this.fileStorageService = fileStorageService;
}
}
이렇게 하면 같은 FileStorageService 타입이라도 원하는 구현체를 정확히 선택할 수 있습니다.
10. 자주 하는 실수
1) Bean 이름을 잘못 작성하는 경우
@Qualifier는 Bean 이름을 기준으로 선택합니다. 그래서 이름을 잘못 쓰면 Spring이 Bean을 찾지 못할 수 있습니다.
public OrderService(@Qualifier("kakaoPay") PayService payService) {
this.payService = payService;
}
그런데 실제 Bean 이름이 kakaoPayService라면 이름이 맞지 않습니다. 이럴 때는 Bean 이름을 정확히 확인해야 합니다.
2) @Primary를 여러 개 붙이는 경우
같은 타입의 Bean 여러 개에 모두 @Primary를 붙이면 기본 Bean이 여러 개가 되어 다시 애매해질 수 있습니다.
@Primary
@Service
public class KakaoPayService implements PayService {
}
@Primary
@Service
public class NaverPayService implements PayService {
}
이렇게 작성하면 Spring은 기본으로 사용할 Bean을 하나로 정하기 어렵습니다. @Primary는 같은 타입 후보 중 보통 하나만 지정하는 것이 좋습니다.
3) 인터페이스와 구현체 관계를 이해하지 못하는 경우
문제가 자주 생기는 지점은 인터페이스 타입으로 주입받을 때입니다.
private final PayService payService;
이때 PayService를 구현한 Bean이 하나라면 큰 문제가 없습니다. 하지만 구현체가 여러 개라면 Spring은 어떤 구현체를 넣어야 할지 판단해야 합니다.
PayService
├─ KakaoPayService
└─ NaverPayService
이런 경우 @Qualifier나 @Primary로 선택 기준을 명확히 해줘야 합니다.
4) DTO나 VO에 사용하려는 경우
DTO나 VO는 보통 데이터를 담는 객체입니다. Spring Bean으로 등록해서 주입받는 대상이 아닙니다.
@Qualifier, @Primary를 주로 사용하는 곳
→ Service 구현체
→ Repository 구현체
→ 외부 API 연동 클래스
→ 파일 저장 방식 클래스
→ 결제 방식 클래스
보통 사용하지 않는 곳
→ DTO
→ VO
→ 단순 요청 객체
→ 단순 응답 객체
11. @Qualifier와 @Primary 흐름 정리
Spring이 Bean을 주입할 때는 먼저 타입을 기준으로 후보를 찾습니다. 후보가 하나라면 그대로 주입할 수 있습니다. 하지만 후보가 여러 개라면 추가 기준이 필요합니다.
1. 주입할 타입 확인
2. Spring Container에서 같은 타입의 Bean 검색
3. Bean이 하나면 바로 주입
4. Bean이 여러 개면 @Primary 확인
5. @Qualifier가 있으면 해당 Bean을 직접 선택
6. 선택된 Bean을 주입
핵심은 이것입니다.
같은 타입 Bean이 여러 개일 때
→ 기본값은 @Primary
→ 직접 지정은 @Qualifier
정리
@Qualifier와 @Primary는 같은 타입의 Bean이 여러 개 있을 때 어떤 Bean을 주입할지 정하기 위해 사용합니다.
| @Qualifier | 주입할 Bean을 이름으로 직접 지정합니다. |
| @Primary | 여러 Bean 중 기본으로 사용할 Bean을 지정합니다. |
@Primary는 기본 선택을 정하는 방식이고, @Qualifier는 특정 Bean을 직접 지정하는 방식입니다. 둘이 함께 있을 경우에는 더 구체적으로 지정한 @Qualifier가 우선합니다.
처음에는 어렵게 느껴질 수 있지만, 기준은 간단합니다.
Bean이 하나다
→ 그냥 주입 가능
같은 타입 Bean이 여러 개다
→ @Primary 또는 @Qualifier 필요
이 개념을 이해하면 Spring에서 의존성 주입 오류가 발생했을 때 원인을 훨씬 빠르게 찾을 수 있습니다.
연습해보기
아래 구조를 보고 어떤 방식이 적절한지 생각해보세요.
NotificationService
├─ EmailNotificationService
├─ SmsNotificationService
└─ KakaoNotificationService
만약 대부분 이메일 알림을 기본으로 사용한다면 EmailNotificationService에 @Primary를 붙일 수 있습니다. 특정 기능에서만 카카오 알림을 사용해야 한다면 해당 생성자 파라미터에 @Qualifier("kakaoNotificationService")를 붙일 수 있습니다.
다음 글에서는 Bean의 Scope에 대해 정리하겠습니다. Spring Bean이 언제 생성되고, 얼마나 오래 유지되는지 살펴보면 Singleton Bean 개념까지 자연스럽게 이어집니다

.

'Tech Stack > Spring Boot' 카테고리의 다른 글
| [Spring] 12. Singleton Bean과 상태값 주의사항 (0) | 2026.07.06 |
|---|---|
| [Spring] 11. Bean Scope란? (0) | 2026.07.05 |
| [Spring] 09. @Autowired와 생성자 주입 (0) | 2026.07.03 |
| [Spring] 08. @Controller, @Service, @Repository (0) | 2026.07.02 |
| [Spring] 07. @Component와 Component Scan (0) | 2026.07.01 |