콘텐츠로 이동

의존관계 자동 주입 (@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 주입됨
}

단점: 파라미터명이 특정 구현체 이름에 묶여버린다. 나중에 rateDiscountPolicymainDiscountPolicy로 리팩터링하면 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 발생

문제: 문자열이라 오타를 컴파일 타임에 잡을 수 없다

@Qualifier("mainDiscountPolicy") // 오타를 내도 컴파일 에러 없음
@Qualifier("mainDiscoutPolicy")  // 오타! → 런타임에 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> 주입이 모든 구현체를 자동으로 수집해주는 이유다.