본문으로 건너뛰기
김신건의 로그

[Spring] @Autowired 해결 전략: @Qualifier, @Primary, byType

· 수정 · 📖 약 2분 · 803자/단어 #spring #di #autowired #qualifier
@Autowired, @Qualifier, @Primary, byType byName, autowire resolution, spring 의존성 해결, ObjectProvider

정의

spring-ioc-di@Autowired기초 (생성자/필드/setter 주입) 를 다룬다면, 이 페이지는 같은 타입의 Bean 이 여러 개일 때 어떻게 하나를 골라내는지 의 해결 전략을 다룬다.

기본 알고리즘

@Autowired 가 후보 Bean 을 찾는 순서:

  1. 타입 일치 (byType): 주입 타입과 호환되는 Bean 들 수집
  2. 후보가 1개면 끝
  3. 후보가 여러 개면:
    • @Primary 가 붙은 Bean 있으면 선택
    • 없으면 @Qualifier 또는 변수명 (byName) 매치
    • 그래도 못 정하면 NoUniqueBeanDefinitionException
  4. 후보가 0개면:
    • required = true (기본) 이면 NoSuchBeanDefinitionException
    • required = false 면 null 주입

시나리오: 같은 타입 2개

public interface NotificationSender { void send(String msg); }

@Component class EmailSender implements NotificationSender { ... }
@Component class SlackSender implements NotificationSender { ... }

@Service
public class AlertService {
    @Autowired
    NotificationSender sender;   // ❌ 어떤 걸 주입?
    // NoUniqueBeanDefinitionException: expected single matching bean but found 2
}

해결 1: @Qualifier

명시적 이름 매칭:

@Component("emailSender") class EmailSender implements NotificationSender { ... }
@Component("slackSender") class SlackSender implements NotificationSender { ... }

@Service
public class AlertService {
    @Autowired
    @Qualifier("slackSender")
    NotificationSender sender;
}

@Component 의 value 가 Bean 이름. 생략하면 클래스명의 lowerCamelCase (EmailSenderemailSender).

해결 2: @Primary

여러 후보 중 기본 표시:

@Component @Primary class EmailSender implements NotificationSender { ... }
@Component class SlackSender implements NotificationSender { ... }

@Service
public class AlertService {
    @Autowired
    NotificationSender sender;   // → EmailSender 주입
}

특정 컨텍스트에서 다른 Bean 이 필요하면 @Qualifier 로 override. @Primary + @Qualifier 조합이 흔한 패턴.

해결 3: byName fallback

@Qualifier 가 없고 @Primary 도 없으면 변수명 이 매칭에 사용된다:

@Service
public class AlertService {
    @Autowired
    NotificationSender slackSender;   // 변수명이 Bean 이름과 일치 → 매칭
}

가독성이 낮아 권장 안 함. @Qualifier 명시가 더 명확.

해결 4: 컬렉션 주입

모든 매칭 Bean 을 다 받기:

@Service
public class AlertService {
    @Autowired
    List<NotificationSender> senders;   // [EmailSender, SlackSender]

    // Map: key = Bean 이름, value = Bean
    @Autowired
    Map<String, NotificationSender> senderMap;

    public void broadcast(String msg) {
        senders.forEach(s -> s.send(msg));
    }
}

@Order / Ordered 인터페이스로 순서 보장:

java
interface Sender { void send(String m); }

@Component @Order(2)
class EmailSender implements Sender { public void send(String m) { System.out.println("email:" + m); } }

@Component @Order(1)
class SlackSender implements Sender { public void send(String m) { System.out.println("slack:" + m); } }

@Service
class AlertService {
  @Autowired List<Sender> senders;
  public void broadcast(String m) { senders.forEach(s -> s.send(m)); }
}

// main
ctx.getBean(AlertService.class).broadcast("deploy ok");
stdout
slack:deploy ok
email:deploy ok
@Component @Order(1) class EmailSender ...
@Component @Order(2) class SlackSender ...

해결 5: Optional 주입

Bean 이 없을 수도 있는 경우:

@Service
public class AlertService {
    @Autowired(required = false)
    NotificationSender sender;   // 없으면 null

    @Autowired
    Optional<NotificationSender> optSender;  // 더 명시적
}

ObjectProvider (가장 유연)

Spring 4.3+ 의 추천 패턴. 위 모든 시나리오를 하나로 통합:

@Service
public class AlertService {
    private final ObjectProvider<NotificationSender> senderProvider;

    public AlertService(ObjectProvider<NotificationSender> p) {
        this.senderProvider = p;
    }

    public void notify(String msg) {
        // 1개 정확히
        senderProvider.getObject().send(msg);
        // 없으면 null
        senderProvider.getIfAvailable().send(msg);
        // 없으면 default 공급자
        senderProvider.getIfAvailable(() -> new NoOpSender()).send(msg);
        // 전부
        senderProvider.orderedStream().forEach(s -> s.send(msg));
        // 호출마다 새 인스턴스 (prototype 함정 해결)
        TaskWorker w = workerProvider.getObject();
    }
}

특히 prototype Bean 을 singleton 안에서 매번 새로 받을 때 가장 깔끔. spring-bean-scope 참고.

@Resource (Jakarta 표준)

@Service
public class AlertService {
    @Resource(name = "slackSender")
    NotificationSender sender;
}

@Autowired 와 차이:

  • 기본이 byName (이름 먼저, 없으면 byType fallback)
  • Jakarta 표준 (jakarta.annotation.Resource)
  • Spring framework 비종속

대부분 @Autowired + @Qualifier 가 일반적.

@Lazy 의존성

순환 의존성 또는 무거운 Bean 의 lazy 초기화:

@Component
public class ServiceA {
    @Autowired
    @Lazy
    ServiceB serviceB;   // 사용 시점에 proxy 가 실제 Bean 조회
}

@Lazy 가 붙으면 컨테이너는 proxy 를 주입하고, 메서드 호출 시점에 실제 Bean 을 lookup. 순환 의존성을 회피할 수 있지만 근본 해결책은 아님 (설계 재검토 권장).

생성자 주입 시 @Autowired 생략

Spring 4.3+: 단일 생성자면 @Autowired 생략 가능:

@Service
public class AlertService {
    private final NotificationSender sender;

    public AlertService(NotificationSender sender) {   // @Autowired 자동
        this.sender = sender;
    }
}

복수 생성자가 있으면 명시 필요. Lombok @RequiredArgsConstructor 가 final 필드 기반으로 생성자를 만들어 주는 게 자주 쓰임.

메타 어노테이션 (Custom Qualifier)

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface SlackChannel {}

@Component @SlackChannel class SlackSender implements NotificationSender { ... }

@Service
public class AlertService {
    @Autowired @SlackChannel
    NotificationSender sender;   // 문자열 키 없이 타입 안전
}

오타 방지, IDE 자동완성. 환경별 (@DevOnly, @ProdOnly) 또는 의미별 (@MainDataSource, @ReadReplica) 구분에 유용.

함정과 베스트 프랙티스

  • 생성자 주입 우선: final 필드, 불변, 테스트 용이성, 순환 의존성 컴파일 타임 감지
  • @Qualifier@Primary 보다 우선: 둘 다 있으면 @Qualifier 가 이김
  • @Primary 는 보수적으로: “기본” 의도가 코드에서 사라지면 혼란
  • 컬렉션 주입은 빈 리스트도 정상: 매칭 Bean 없으면 empty list (예외 안 던짐)
  • Optional<T>required=false 보다 명시적
  • prototype 을 singleton 에 주입하면 ObjectProvider 또는 ScopedProxy (spring-bean-scope 참고)
  • 순환 의존성 발견 시 @Lazy 보다 설계 재검토: 일반적으로 한 쪽을 추출하거나 이벤트로 분리

관련 위키

이 글의 용어 (5개)
[Spring] @Conditional, Profile, @ConfigurationPropertiesspring
정의 Spring Boot는 조건부 Bean 등록 메커니즘을 제공한다. classpath, property, 다른 Bean 유무 등에 따라 자동 구성을 활성화/비활성화. , 어노…
[Spring] Bean Lifecycle: 초기화와 소멸 콜백spring
정의 가 Bean 등록과 주입 을 다룬다면, 이 페이지는 등록 이후 생성 → 사용 → 소멸 사이에 끼어드는 콜백을 다룬다. Spring 컨테이너가 단순히 객체를 만들고 끝내는 게…
[Spring] Bean Scope 심화: Singleton, Prototype, Web Scopespring
정의 가 5가지 scope 의 개요 를 다룬다면, 이 페이지는 그 중 함정 (prototype-into-singleton) 과 해결 패턴 (ScopedProxy, ObjectPr…
[Spring] IoC와 DI: Bean, ApplicationContext, @Autowiredspring
정의 IoC (Inversion of Control)는 객체 생성/생명주기 제어를 프레임워크가 가져가는 원칙. DI (Dependency Injection)는 그 구현 방법. S…
[Spring] Stereotype 어노테이션: @Component, @Service, @Repository, @Controllerspring
정의 Stereotype 어노테이션 은 클래스가 Spring 컨테이너의 어떤 역할 (role) 인지를 의미적으로 표시한다. 모두 를 메타 어노테이션 으로 가진 specializa…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기