@Autowired가 어떤 bean을 선택하는가 — 의존성 주입 패턴과 충돌 해결
PaymentGateway 인터페이스를 구현한 bean이 3개다 — StripeGateway, TossGateway, MockGateway. Spring에 @Autowired PaymentGateway gateway를 요청하면 어떤 bean이 주입되는가? Spring은 타입으로 찾고, 이름으로 구분하고, 여전히 모호하면 예외를 던진다.
이 글은 세 가지 주입 방식(생성자, 세터, 필드)의 차이, @Qualifier로 모호성을 해결하는 방법, 그리고 @Lazy와 선택적 주입(Optional) 패턴을 풀어간다. (Spring Framework Reference - DI)
세 가지 주입 패턴
1. 생성자 주입 (권장)
// Spring 7 / Java 25 — 생성자 주입
@Service
public class OrderService {
private final PaymentGateway gateway;
private final MailSender mailer;
public OrderService(PaymentGateway gateway, MailSender mailer) {
this.gateway = gateway;
this.mailer = mailer;
}
}
| 장점 | 단점 |
|---|---|
final 필드 (불변) |
모든 의존성을 한 번에 전달해야 함 |
| 순환 의존성 즉시 감지 | — |
| 컴파일 타임에 의존성 명확 | — |
| 테스트에서 mock 주입 용이 | — |
왜 final이 중요한가? final 필드는 생성자 호출 후 변경이 불가능하다 — 다중 스레드 환경에서 안전하다(JMM의 happens-before 보장). 필드 주입은 final을 쓸 수 없으므로, 언제든 값이 변경될 수 있어 thread safety가 깨진다. Spring의 singleton bean은 여러 스레드가 동시에 접근하므로, 불변성은 단순한 스타일이 아니라 정확성의 문제다.
비유: 생성자 주입은 한 번 잠근 금고다. 생성 시 모든 것을 넣고 잠그면, 그 후에는 누구도 내용물을 바꿀 수 없다. 필드 주입은 열쇠가 달린 서랍이다 — 언제든 누군가가 내용물을 바꿀 수 있다. 여러 사람(스레드)이 동시에 서랍에 접근하면 누가 마지막으로 바꿨는지 보장할 수 없다.
2. 세터 주입
// Spring 7 / Java 25 — 세터 주입
@Service
public class OrderService {
private PaymentGateway gateway;
private MailSender mailer;
@Autowired
public void setGateway(PaymentGateway gateway) {
this.gateway = gateway;
}
@Autowired
public void setMailer(MailSender mailer) {
this.mailer = mailer;
}
}
세터 주입은 선택적 의존성에 적합하다 — 주입하지 않아도 객체가 생성된다. @Autowired(required = false)로 선택적으로 만들 수도 있다.
3. 필드 주입 (비권장)
// Spring 7 / Java 25 — 필드 주입 (비권장)
@Service
public class OrderService {
@Autowired
private PaymentGateway gateway; // final 불가 → 불변성 없음
@Autowired
private MailSender mailer;
}
| 단점 |
|---|
final 불가 → 불변성 보장 안 됨 |
| 의존성이 숨겨짐 (생성자 시그니처로 파악 안 됨) |
| 테스트에서 mock을 주입하려면 리플렉션 필요 |
| 순환 의존성이 조용히 해결됨 (문제를 숨김) |
Spring Framework 4.x부터 필드 주입에 대해 경고를 낸다. Spring Boot의
spring.main.allow-circular-references=false기본값과 함께, 필드 주입을 쓰면 설계 문제를 숨기게 된다.
@Autowired의 타입 매칭 — 어떤 bean이 선택되는가
@Autowired는 타입 먼저 매칭한다. 같은 타입의 bean이 여러 개면:
flowchart TD
A["@Autowired<br/>PaymentGateway"] --> B{같은 타입 bean 수}
B -->|1개| OK1["주입 완료"]
B -->|여러 개| C{필드명/매개변수명과<br/>bean 이름 일치?}
C -->|일치| OK2["이름으로 선택"]
C -->|불일치| D{"@Qualifier 있음?"}
D -->|있음| OK3["@Qualifier 값으로 선택"]
D -->|없음| E{"@Primary 있음?"}
E -->|있음| OK4["@Primary bean 선택"]
E -->|없음| FAIL["NoUniqueBeanDefinitionException"]
@Qualifier — 명시적 bean 선택
// Spring 7 / Java 25 — @Qualifier
@Service
public class OrderService {
private final PaymentGateway gateway;
public OrderService(@Qualifier("stripe") PaymentGateway gateway) {
this.gateway = gateway;
}
}
@Configuration
public class GatewayConfig {
@Bean
@Qualifier("stripe")
public PaymentGateway stripeGateway() { return new StripeGateway(); }
@Bean
@Qualifier("toss")
public PaymentGateway tossGateway() { return new TossGateway(); }
}
@Primary — 기본 bean 지정
// Spring 7 / Java 25 — @Primary
@Bean
@Primary // 같은 타입 중 이 bean이 기본
public PaymentGateway stripeGateway() { return new StripeGateway(); }
@Bean
public PaymentGateway tossGateway() { return new TossGateway(); }
// OrderService에 @Qualifier 없이 주입하면 stripeGateway가 선택됨
@Primary는 "기본값"을 지정할 때,@Qualifier는 "명시적 선택"을 할 때 쓴다. 프로덕션에서는@Qualifier가 의도를 더 명확히 전달한다 —@Primary는 어떤 bean이 주입되는지 코드만 보고 알기 어렵다.
컬렉션 주입 — 모든 구현체 받기
// Spring 7 / Java 25 — 모든 PaymentGateway 구현체 주입
@Service
public class PaymentRouter {
private final List<PaymentGateway> gateways;
private final Map<String, PaymentGateway> gatewayMap;
public PaymentRouter(List<PaymentGateway> gateways,
Map<String, PaymentGateway> gatewayMap) {
// gateways: 모든 PaymentGateway bean이 리스트로 주입됨
// gatewayMap: bean 이름 → bean 객체 맵
this.gateways = gateways;
this.gatewayMap = gatewayMap;
}
}
@Lazy — 지연 초기화
// Spring 7 / Java 25 — @Lazy로 순환 의존성 회피 (근본 해결 아님)
@Service
public class A {
private final B b;
public A(@Lazy B b) { this.b = b; } // B의 프록시를 주입, 실제 B는 나중에 생성
}
@Lazy는 bean이 실제로 사용될 때까지 생성을 미룬다. 순환 의존성을 회피하는 임시 방편으로 쓸 수 있지만, 근본 원인(설계 문제)을 해결하지는 않는다.
실습 — DI 패턴 비교
// Spring 7 / Java 25 — DiPatternsDemo.java
public class DiPatternsDemo {
interface Notifier { void notify(String msg); }
static class EmailNotifier implements Notifier {
public void notify(String msg) { System.out.println("Email: " + msg); }
}
static class SmsNotifier implements Notifier {
public void notify(String msg) { System.out.println("SMS: " + msg); }
}
// 생성자 주입 (권장)
static class ConstructorInjection {
private final Notifier notifier;
public ConstructorInjection(Notifier notifier) { this.notifier = notifier; }
public void run() { notifier.notify("생성자 주입"); }
}
// 세터 주입
static class SetterInjection {
private Notifier notifier;
public void setNotifier(Notifier notifier) { this.notifier = notifier; }
public void run() { notifier.notify("세터 주입"); }
}
public static void main(String[] args) {
Notifier email = new EmailNotifier();
Notifier sms = new SmsNotifier();
// 수동 조립 (Spring이 자동으로 하는 일)
new ConstructorInjection(email).run();
var setter = new SetterInjection();
setter.setNotifier(sms);
setter.run();
}
}
java DiPatternsDemo.java
Email: 생성자 주입
SMS: 세터 주입
확인할 것: 생성자 주입은 객체 생성 시점에 모든 의존성이 확정된다. 세터 주입은 객체 생성 후 의존성을 교체할 수 있다 — 유연하지만 불변성이 깨진다.
@Value — 프로퍼티 주입
// Spring 7 / Java 25 — @Value로 설정값 주입
@Service
public class PaymentService {
@Value("${payment.api.key}")
private String apiKey;
@Value("${payment.timeout:5000}") // 기본값 5000
private long timeout;
}
@Value는 간단하지만 타입 안전하지 않다 — 오타가 있어도 컴파일 타임에 잡히지 않는다. Spring Boot에서는@ConfigurationProperties를 권장한다 (Spring Boot 장 09).
Optional<T> 주입 — 선택적 의존성
// Spring 7 / Java 25 — Optional 주입
@Service
public class OrderService {
private final Optional<NotificationService> notifier;
public OrderService(Optional<NotificationService> notifier) {
this.notifier = notifier;
// NotificationService bean이 없어도 주입됨 (Optional.empty())
}
public void placeOrder(Order order) {
// ...
notifier.ifPresent(n -> n.send(order)); // 있을 때만 실행
}
}
@Autowired 메서드 주입 — 복수 의존성 한 번에
// Spring 7 / Java 25 — 메서드에 @Autowired
@Service
public class MigrationService {
@Autowired
public void configure(
@Qualifier("source") DataSource source,
@Qualifier("target") DataSource target,
@Value("${batch.size:1000}") int batchSize) {
// 복수 의존성 + 프로퍼티를 한 메서드에서 주입
}
}
요약 — 이 글의 결론
- 생성자 주입이 권장된다.
final필드, 순환 의존성 즉시 감지, 테스트 용이. 생성자가 1개면@Autowired생략 가능. - 필드 주입은 피한다.
final불가, 의존성 숨김, 테스트 어려움, 순환 의존성 조용히 허용. - 같은 타입의 bean이 여러 개면
@Qualifier로 명시적 선택한다.@Primary는 기본값 지정이지만, 의도가 덜 명확하다. - 컬렉션(List, Map) 주입으로 모든 구현체를 받을 수 있다. 전략 패턴이나 라우팅 패턴에 유용.
@Lazy는 순환 의존성의 임시 방편이지 근본 해결이 아니다. 설계를 재검토하여 순환을 제거하는 것이 올바른 해결책이다.
생각해 볼 문제
@Autowired(required = false)를 생성자 주입에서 쓸 수 있는가? 세터 주입과의 차이는?List<PaymentGateway>에 주입되는 bean의 순서는 보장되는가?- 같은 타입의 bean이 2개 있을 때, 필드명이 bean 이름과 같으면 Spring이 자동으로 선택하는가?
@Lazy프록시는 JDK 동적 프록시인가 CGLIB 프록시인가?- Spring Boot에서
spring.main.allow-circular-references=true를 설정하면 어떤 일이 일어나는가? 왜 기본이 false인가? @Value("${db.url:localhost:5432}")에서:다음은 기본값이다. 기본값 없이 프로퍼티가 없으면 어떻게 되는가?- 생성자가 2개 이상일 때 Spring은 어느 생성자로 주입하는가?
@Autowired를 어느 생성자에 붙여야 하는가?
참고
- Spring Framework 7 - DI - 접근 2026-07-10
- Spring Framework 7 - @Autowired - 접근 2026-07-10
- Spring Framework 7 - @Qualifier - 접근 2026-07-10
- Spring Framework 7 - @Value - 접근 2026-07-10
'Develop Artifacts > Spring' 카테고리의 다른 글
| Spring - 06. aop (0) | 2026.07.12 |
|---|---|
| Spring - 05. bean scope profile (0) | 2026.07.12 |
| Spring - 04. configuration component-scan (0) | 2026.07.12 |
| Spring - 02. container bean lifecycle (0) | 2026.07.12 |
| Spring - 01. IoC DI (0) | 2026.07.12 |
