[Spring] 10. @Qualifier와 @Primary

[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 개념까지 자연스럽게 이어집니다

.