7장. 스프링 의존관계 자동 주입(Auto-wiring) 완전 정복

스프링을 스프링답게 만들어주는 핵심 기능이 있다.

바로 의존관계 자동 주입(Auto-wiring)이다.

“스프링이 객체를 대신 만들어주고 필요한 곳에 자동으로 넣어주는 기능”

이라는 설명은 많이 들어봤을 것이다.

하지만 실제로 스프링이 의존성을

어떻게 찾고,

어떤 기준으로 채워 넣고,

어떤 알고리즘으로 후보를 선택하며,

왜 생성자 주입이 정답인지,

순환 참조는 왜 발생하는지

같은 핵심 원리는 많은 개발자가 제대로 이해하지 못한다.

이번 7장에서는 이 모든 과정을 쉽게 → 실무 관점 → 내부 원리까지 완전하게 정리해준다.


1. 의존관계 자동 주입이 필요한 이유

스프링을 사용하지 않는다면 다음과 같이 직접 객체를 조립해야 한다.

MemberRepository repository = new MemoryMemberRepository();
MemberService service = new MemberServiceImpl(repository);

서비스가 많아질수록 조립 코드(AppConfig)가 폭발적으로 증가한다.

그뿐 아니라 변경될 때마다 모든 조립 코드를 찾아 수정해야 한다.

스프링은 이런 수고를 없애기 위해 필요한 객체를 자동으로 찾아서 넣어주는 기능을 제공한다.

그것이 바로 @Autowired다.


2. @Autowired로 주입하는 4가지 방식

스프링은 다음 네 가지 방식으로 DI를 제공한다.

  1. 생성자 주입 (가장 권장)
  2. 필드 주입
  3. Setter 주입
  4. 일반 메서드 주입

우선 이 네 가지가 어떤 방식인지 설명한 뒤,

아래에서 기술적으로 왜 생성자 주입만 정답인지 파헤친다.


2-1. 생성자 주입 (Constructor Injection) – 무조건 기본

@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
                        DiscountPolicy discountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
}

장점:

  • 불변 의존성 보장
  • 테스트 코드 작성 쉬움
  • 누락 방지 (컴파일 타임에서 잡힘)
  • 순환참조 감지 가능
  • 객체가 완성된 상태로만 사용됨

2-2. 필드 주입 – 절대 사용 금지

@Autowired
private MemberRepository memberRepository;

왜 금지?

  • 테스트 환경에서 mock 주입 불가
  • 리플렉션 기반 setAccessible(true)로 강제로 필드 주입 → 매우 위험
  • 의존성이 외부에서 보이지 않음
  • 강결합 구조
  • 유지보수 최악

2-3. Setter 주입 – 선택적 의존성일 때만 사용

@Autowired
public void setDiscountPolicy(DiscountPolicy discountPolicy) {
    this.discountPolicy = discountPolicy;
}
  • 선택적 DI에는 좋음
  • 그러나 외부에서 언제든 바꿀 수 있기 때문에 필수 의존성에는 금지

2-4. 일반 메서드 주입 – 거의 사용하지 않음

@Autowired
public void init(MemberRepository repo, DiscountPolicy policy) {}
  • 여러 의존성을 한 번에 초기화할 때 가능
  • 코드 가독성이 떨어져 거의 사용되지 않음

3. 기술 심층: @Autowired 내부 동작 원리

여기서부터는 스프링이 내부적으로 DI를 어떻게 처리하는지

BeanFactory → AutowireCandidateResolver → 빈 탐색 → 생성자 선택 → 주입

이 전체 흐름을 기술적으로 설명한다.


3-1. 자동 주입의 실제 호출 흐름

스프링에서 @Autowired는 단순 어노테이션이 아니다.

내부적으로 다음 클래스를 통해 처리된다.

AutowiredAnnotationBeanPostProcessor DI의 핵심 처리기이며, 다음 순서로 동작한다.

  1. 빈의 생성자 목록 확인
  2. 주입할 후보 빈 탐색
  3. 생성자 우선순위 결정
  4. 필요한 빈 인스턴스 검색
  5. 생성자 인자 주입
  6. BeanPostProcessor 후처리 실행

즉, @Autowired는 BeanPostProcessor 중 하나로 동작한다.


3-2. “타입 → 이름 → Qualifier → Primary” 순서로 후보를 좁힌다

Autowired의 후보 선정 알고리즘:

  1. 타입으로 먼저 빈 목록 조회 List<BeanDefinition> candidates = findBeansOfType(type);
  2. 타입 중복 시 이름으로 재검색
    • 파라미터 이름 또는 필드 이름을 기준
  3. @Qualifier가 있으면 가장 높은 우선순위
  4. @Primary 지정된 빈이 우선 채택

3-3. 생성자가 여러 개 있을 때 스프링의 선택 기준

스프링은 다음 기준으로 “주입할 생성자”를 선택한다.

  1. @Autowired가 붙은 생성자
  2. 생성자가 한 개라면 자동으로 선택
  3. 생성자가 여러 개 + @Autowired 없음 → 가장 파라미터가 많은 생성자 선택

코드 레벨로 보면:

determineCandidateConstructors(Class<?> beanClass)

이 메서드가 생성자 선택을 담당한다.


3-4. 순환 참조가 왜 발생하는가?

A → B

B → A 구조를 봐보자.

생성자 주입을 사용할 경우:

  • A 생성자 호출하려면 B 필요
  • B 생성자 호출하려면 A 필요
  • 서로 필요하기 때문에 인스턴스를 만들 수 없음 → 즉시 예외 발생

필드/Setter 주입은 순환 참조가 일단 가능했음

(초기 DI 단계에서 프록시 인스턴스 미리 생성)

그러나 스프링 2.6 이후 기본 정책은:

생성자·필드·Setter 어느 방식이든 순환 참조 기본적으로 금지


4. 동일한 타입의 빈이 여러 개 있을 때 문제 해결

예: DiscountPolicy가 두 개

  • FixDiscountPolicy
  • RateDiscountPolicy

@Autowired로 타입만 주입하면 에러 발생:

NoUniqueBeanDefinitionException

스프링은 이렇게 해결한다.


4-1. @Primary – 기본 우선순위 지정

@Primary
@Component
public class RateDiscountPolicy implements DiscountPolicy {}

4-2. @Qualifier – 명시적 선택

@Autowired
public OrderServiceImpl(@Qualifier("mainPolicy") DiscountPolicy policy) {
    this.policy = policy;
}

4-3. Map/List 주입 – 전략 패턴 구현 시 유용

@Autowired
private Map<String, DiscountPolicy> policyMap;

스프링은 모든 빈을 Map 형식으로 자동 주입한다.


5. Spring이 DI를 실패하면 어떤 예외가 발생하는가?

주요 예외:

1. NoSuchBeanDefinitionException

주입할 빈이 존재하지 않음

2. NoUniqueBeanDefinitionException

타입 일치 빈이 여러 개 존재

3. BeanCurrentlyInCreationException

순환 참조 발생

4. UnsatisfiedDependencyException

생성자 파라미터 타입 불일치


6. Lombok + 생성자 주입이 실무 정석인 이유

실무에서는 다음 방식이 사실상 표준이다.

@RequiredArgsConstructor
@Service
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
}

이유:

  • final로 불변성 확보
  • 생성자 자동 생성
  • 주입 누락 방지
  • DI가 분명해지고 분석이 쉬움

7. 결론: 생성자 주입 + 스프링의 자동 탐색 알고리즘이 정답이다

요약하면:

  • @Autowired는 단순 어노테이션이 아니라 BeanPostProcessor → CandidateResolver → 생성자 선택 → 주입 의 복잡한 알고리즘이 동작한다.
  • 생성자 주입은 불변성, 안정성, 순환 참조 방지, 테스트 편의성까지 모든 면에서 우수하다.
  • 실제 실무에서는 “생성자 주입 + Lombok” 조합이 스프링 개발의 표준이다.
  • 스프링의 자동 주입 알고리즘을 이해하면 유지보수, 확장, 디버깅 능력이 확연히 올라간다.