의존관계 자동 주입 (@Autowired)
@Autowired 기본 동작
@Autowired는 스프링 컨테이너에서 타입이 일치하는 빈을 찾아 주입한다.
내부적으로 ac.getBean(MemberRepository.class)와 동일하게 동작한다.
@Component
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
@Autowired
public MemberServiceImpl(MemberRepository memberRepository) {
// 스프링이 MemberRepository 타입 빈을 컨테이너에서 찾아서 여기에 주입
this.memberRepository = memberRepository;
}
}
주입 흐름:
@Autowired 발견
↓
스프링 컨테이너에서 해당 타입(MemberRepository)으로 빈 조회
↓
빈 1개 → 주입 성공 ✓
빈 0개 → NoSuchBeanDefinitionException
(@Autowired(required=false) 설정 시 주입 생략)
빈 2개↑ → NoUniqueBeanDefinitionException → 추가 처리 필요
@Autowired 옵션 처리 — 빈이 없을 때
주입 대상 빈이 스프링 컨테이너에 없어도 동작해야 하는 경우가 있다. 예: 메일 서버가 없는 환경에서도 앱이 돌아가야 하는 경우.
// 방법 1: required = false
// 빈이 없으면 이 setter 메서드 자체가 호출되지 않음 (mailSender = null 유지)
@Autowired(required = false)
public void setMailSender(MailSender mailSender) {
this.mailSender = mailSender;
}
// 방법 2: @Nullable
// 빈이 없으면 null이 주입됨 (메서드는 호출됨)
@Autowired
public void setMailSender(@Nullable MailSender mailSender) {
this.mailSender = mailSender; // mailSender가 null일 수 있음
}
// 방법 3: Optional
// 빈이 없으면 Optional.empty() 주입
@Autowired
public void setMailSender(Optional<MailSender> mailSender) {
this.mailSender = mailSender.orElse(null);
}
| 방법 | 빈 없을 때 | 빈 있을 때 |
|---|---|---|
required=false |
메서드 호출 안 함 | 정상 주입 |
@Nullable |
null 주입 |
정상 주입 |
Optional<T> |
Optional.empty() |
Optional.of(빈) |
빈이 2개 이상일 때 문제
같은 인터페이스를 구현한 빈이 두 개 등록되어 있으면 @Autowired가 어느 것을 주입해야 할지 모른다.
// 인터페이스
public interface DiscountPolicy { int discount(Member member, int price); }
// 구현체 1
@Component
public class FixDiscountPolicy implements DiscountPolicy { ... }
// 구현체 2
@Component
public class RateDiscountPolicy implements DiscountPolicy { ... }
// @Autowired로 주입받으려 하면
@Autowired
private DiscountPolicy discountPolicy;
// → NoUniqueBeanDefinitionException:
// expected single matching bean but found 2: fixDiscountPolicy, rateDiscountPolicy
이 상황을 해결하는 방법이 3가지 있다.
해결 방법 1: 필드명/파라미터명 매칭
@Autowired는 타입 매칭에 실패했을 때(같은 타입이 2개 이상),
필드명 또는 파라미터명을 빈 이름으로 추가 매칭한다.
// 방법: 필드명 또는 파라미터명을 원하는 빈 이름과 일치시킨다
// 필드 주입의 경우
@Autowired
private DiscountPolicy rateDiscountPolicy; // 필드명 = "rateDiscountPolicy" → 이 이름의 빈 주입
// 생성자 주입의 경우
@Autowired
public OrderServiceImpl(DiscountPolicy rateDiscountPolicy) { // 파라미터명 = "rateDiscountPolicy"
this.discountPolicy = rateDiscountPolicy; // RateDiscountPolicy 주입됨
}
단점: 파라미터명이 특정 구현체 이름에 묶여버린다.
나중에 rateDiscountPolicy를 mainDiscountPolicy로 리팩터링하면 DI가 깨질 수 있다.
컴파일 에러가 나지 않아서 실수를 발견하기 어렵다.
해결 방법 2: @Qualifier (추가 구별자)
빈에 추가 이름표(Qualifier)를 붙이고, 주입 시 그 이름표로 지정한다.
@Autowired의 1순위는 타입이고, 2순위는 @Qualifier다.
// 빈에 @Qualifier 붙이기
@Component
@Qualifier("mainDiscountPolicy") // "mainDiscountPolicy"라는 추가 이름표
public class RateDiscountPolicy implements DiscountPolicy { ... }
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy { ... }
// 주입할 때 @Qualifier로 지정
@Autowired
public OrderServiceImpl(@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy; // RateDiscountPolicy 주입됨
}
@Qualifier 매칭 순서
1. @Qualifier("mainDiscountPolicy") 값으로 빈의 Qualifier를 탐색
2. 없으면 "mainDiscountPolicy" 이름의 빈을 탐색
3. 없으면 NoSuchBeanDefinitionException 발생
문제: 문자열이라 오타를 컴파일 타임에 잡을 수 없다
해결: 커스텀 어노테이션으로 타입 안전하게 만들기
// 커스텀 어노테이션 정의
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER,
ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier("mainDiscountPolicy") // 내부에 @Qualifier 포함
public @interface MainDiscountPolicy {}
// 빈에 커스텀 어노테이션 적용
@Component
@MainDiscountPolicy // 오타 시 컴파일 에러! → 안전
public class RateDiscountPolicy implements DiscountPolicy { ... }
// 주입 시에도 커스텀 어노테이션 사용
@Autowired
public OrderServiceImpl(@MainDiscountPolicy DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
커스텀 어노테이션을 쓰면 오타를 컴파일 타임에 잡을 수 있고, IDE 자동완성도 지원된다.
해결 방법 3: @Primary (우선순위)
여러 빈 중 하나에 @Primary를 붙이면, 타입 매칭 시 이 빈이 우선으로 선택된다.
매번 @Qualifier를 붙이지 않아도 되어 편리하다.
@Component
@Primary // 타입으로 주입 시 이 빈이 기본으로 선택됨
public class RateDiscountPolicy implements DiscountPolicy { ... }
@Component
public class FixDiscountPolicy implements DiscountPolicy { ... }
// @Primary가 있으면 타입 그대로 주입 가능
@Autowired
private DiscountPolicy discountPolicy; // → RateDiscountPolicy 주입됨
@Primary vs @Qualifier — 우선순위
@Qualifier와 @Primary가 동시에 있으면 @Qualifier가 더 우선이다.
타입 매칭으로 여러 빈이 나오면:
@Qualifier 있음 → @Qualifier 매칭 (더 구체적)
@Qualifier 없음 → @Primary 있는 빈 선택
@Primary 없음 → 필드명/파라미터명 매칭
모두 해당 없음 → NoUniqueBeanDefinitionException
실무 활용 패턴
// 주 DB — @Primary (평소에 항상 쓰는 것)
@Bean
@Primary
public DataSource mainDataSource() {
HikariDataSource ds = new HikariDataSource();
ds.setJdbcUrl("jdbc:mysql://main-db:3306/mydb");
return ds;
}
// 서브 DB — @Qualifier (통계, 배치 등 특별히 필요할 때만)
@Bean
@Qualifier("subDataSource")
public DataSource subDataSource() {
HikariDataSource ds = new HikariDataSource();
ds.setJdbcUrl("jdbc:mysql://sub-db:3306/mydb");
return ds;
}
// 일반 서비스: @Primary인 mainDataSource 자동 주입
@Autowired
private DataSource dataSource; // mainDataSource
// 통계 서비스: @Qualifier로 subDataSource 지정
@Autowired
@Qualifier("subDataSource")
private DataSource subDataSource;
모든 구현체가 필요할 때: List, Map
런타임에 구현체를 동적으로 선택해야 하는 전략 패턴에서 유용하다.
DiscountPolicy 구현체가 여러 개일 때, 클라이언트가 선택한 정책에 따라 적용하는 방법.
@Service
public class DiscountService {
// DiscountPolicy를 구현한 모든 빈을 주입받음
private final Map<String, DiscountPolicy> policyMap;
private final List<DiscountPolicy> policies;
@Autowired
public DiscountService(Map<String, DiscountPolicy> policyMap,
List<DiscountPolicy> policies) {
this.policyMap = policyMap;
this.policies = policies;
// policyMap = {
// "fixDiscountPolicy" : FixDiscountPolicy@x01,
// "rateDiscountPolicy" : RateDiscountPolicy@x02
// }
// policies = [FixDiscountPolicy@x01, RateDiscountPolicy@x02]
}
// 클라이언트가 선택한 할인 코드에 따라 동적으로 처리
public int discount(Member member, int price, String discountCode) {
DiscountPolicy discountPolicy = policyMap.get(discountCode);
if (discountPolicy == null) {
throw new IllegalArgumentException("존재하지 않는 할인 코드: " + discountCode);
}
return discountPolicy.discount(member, price);
}
}
// 사용
discountService.discount(member, 10000, "fixDiscountPolicy"); // 정액 할인
discountService.discount(member, 10000, "rateDiscountPolicy"); // 정률 할인
새로운 할인 정책 NewYearDiscountPolicy를 추가하면 @Component만 붙이면 된다.
DiscountService 코드는 건드리지 않아도 된다.
OCP(개방-폐쇄 원칙) 준수: 새 구현체 추가 시 기존 코드 변경 없음.
@Autowired 매칭 전체 흐름
우선순위: 타입 → @Qualifier → @Primary → 필드명/파라미터명
1. 타입(Type) 매칭 시도
↓ 빈이 1개면 바로 주입 성공
↓ 빈이 2개 이상이면 아래로
2. @Qualifier 확인
↓ @Qualifier 있으면 해당 값으로 매칭 → 주입 성공
↓ 없으면 아래로
3. @Primary 확인
↓ @Primary 있는 빈이 있으면 주입 성공
↓ 없으면 아래로
4. 필드명 / 파라미터명으로 빈 이름 매칭
↓ 이름이 일치하는 빈 있으면 주입 성공
↓ 없으면
5. NoUniqueBeanDefinitionException 발생
자동 vs 수동 등록 선택 기준
| 상황 | 권장 방식 | 이유 |
|---|---|---|
| 일반 비즈니스 로직 (서비스, 레포지토리) | 자동 (@ComponentScan) |
수가 많고 패턴이 명확해서 자동화가 효율적 |
| 기술 지원 빈 (AOP, DataSource 등) | 수동 (@Bean) |
명확히 드러내야 관리가 쉬움 |
| 다형성 활용 (구현체 여러 개) | 수동 or @Qualifier |
어떤 빈이 있는지 한눈에 파악 가능해야 함 |
| 외부 라이브러리 빈 | 수동 (@Bean) |
내 코드가 아니라 어노테이션 추가 불가 |
// 다형성 빈 관리 — 수동 등록이 명확함
@Configuration
public class DiscountPolicyConfig {
// 어떤 할인 정책 빈이 있는지 이 파일 하나를 보면 바로 알 수 있음
@Bean
public DiscountPolicy rateDiscountPolicy() { return new RateDiscountPolicy(); }
@Bean
public DiscountPolicy fixDiscountPolicy() { return new FixDiscountPolicy(); }
}
자동 등록만 사용하면 어떤 구현체들이 등록되어 있는지 파악하기 위해 전체 코드를 뒤져야 한다. 수동 등록으로 설정 파일에 모아두면 한 곳에서 관리된다.
내부 동작 원리
@Autowired는 어떻게 처리되는가
@Autowired를 실제로 처리하는 클래스는 AutowiredAnnotationBeanPostProcessor다.
스프링 컨테이너가 빈을 생성한 직후, 이 처리기가 postProcessProperties()를 호출해 의존관계를 주입한다.
빈 생성 (Constructor 호출) 완료
↓
AutowiredAnnotationBeanPostProcessor.postProcessProperties() 호출
1. 빈 클래스를 리플렉션으로 분석
→ 모든 필드, 메서드를 스캔
→ @Autowired / @Value / @Inject 발견 시 목록 수집
2. 각 주입 대상에 대해:
→ 필드 타입(또는 파라미터 타입)으로 DefaultListableBeanFactory에 빈 요청
→ 해당 타입 빈이 1개면 즉시 반환
→ 2개 이상이면 @Qualifier, @Primary, 이름 순서로 좁히기
3. 리플렉션으로 실제 주입:
field.setAccessible(true) // private 필드도 접근 허용
field.set(bean, resolvedBean) // 빈 주입
↓
주입 완료 → 생명주기 콜백 (@PostConstruct 등) 실행
// 실제 처리 흐름 (의사코드)
class AutowiredAnnotationBeanPostProcessor {
public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
// 1. 이 빈 클래스의 @Autowired 메타데이터 캐시에서 조회 (또는 새로 수집)
InjectionMetadata metadata = findAutowiringMetadata(bean.getClass());
// 2. 각 주입 포인트에 대해 처리
for (InjectedElement element : metadata.injectedElements) {
// 3. 타입으로 빈 찾기
Object value = beanFactory.resolveDependency(
new DependencyDescriptor(element.field, true), // 필드 정보
beanName
);
// 4. 리플렉션으로 주입
Field field = element.field;
field.setAccessible(true); // private 접근 허용
field.set(bean, value); // 실제 주입
}
return pvs;
}
}
왜
setAccessible(true)가 필요한가? 자바의 접근 제어자(private)는 일반 코드에서는 접근을 막는다. 리플렉션 API의setAccessible(true)는 접근 제어자를 런타임에 무력화한다. 이 덕분에private DiscountPolicy discountPolicy처럼 private 필드에도@Autowired가 동작한다.
제네릭 타입 주입 — ResolvableType
List<DiscountPolicy> 주입 시 문제가 있다. 자바는 런타임에 제네릭 타입 정보를 지우는 타입 소거(Type Erasure)를 한다.
// 컴파일 후 바이트코드에서는 이 두 줄이 동일하게 보임
List<DiscountPolicy> policies1;
List<MemberRepository> policies2;
// 런타임에는 둘 다 그냥 List로 처리
스프링은 이 문제를 ResolvableType으로 해결한다:
// 스프링 내부 처리 (의사코드)
// 필드: private List<DiscountPolicy> policies
// 1. 필드의 제네릭 타입 정보를 ResolvableType으로 추출
ResolvableType resolvableType = ResolvableType.forField(field);
// → ResolvableType{List<DiscountPolicy>}
// 2. 제네릭 파라미터 타입 추출
Class<?> elementType = resolvableType.getGeneric(0).resolve();
// → DiscountPolicy.class
// 3. 컨테이너에서 DiscountPolicy 타입 빈을 모두 조회
Map<String, DiscountPolicy> beansOfType = beanFactory.getBeansOfType(DiscountPolicy.class);
// → {fixDiscountPolicy: FixDiscountPolicy@x01, rateDiscountPolicy: RateDiscountPolicy@x02}
// 4. 리스트로 변환하여 주입
policies = new ArrayList<>(beansOfType.values());
이것이
List<DiscountPolicy>주입이 모든 구현체를 자동으로 수집해주는 이유다.