스프링을 스프링답게 만들어주는 핵심 기능이 있다.
바로 의존관계 자동 주입(Auto-wiring)이다.
“스프링이 객체를 대신 만들어주고 필요한 곳에 자동으로 넣어주는 기능”
이라는 설명은 많이 들어봤을 것이다.
하지만 실제로 스프링이 의존성을
어떻게 찾고,
어떤 기준으로 채워 넣고,
어떤 알고리즘으로 후보를 선택하며,
왜 생성자 주입이 정답인지,
순환 참조는 왜 발생하는지
같은 핵심 원리는 많은 개발자가 제대로 이해하지 못한다.
이번 7장에서는 이 모든 과정을 쉽게 → 실무 관점 → 내부 원리까지 완전하게 정리해준다.
1. 의존관계 자동 주입이 필요한 이유
스프링을 사용하지 않는다면 다음과 같이 직접 객체를 조립해야 한다.
MemberRepository repository = new MemoryMemberRepository();
MemberService service = new MemberServiceImpl(repository);
서비스가 많아질수록 조립 코드(AppConfig)가 폭발적으로 증가한다.
그뿐 아니라 변경될 때마다 모든 조립 코드를 찾아 수정해야 한다.
스프링은 이런 수고를 없애기 위해 필요한 객체를 자동으로 찾아서 넣어주는 기능을 제공한다.
그것이 바로 @Autowired다.
2. @Autowired로 주입하는 4가지 방식
스프링은 다음 네 가지 방식으로 DI를 제공한다.
- 생성자 주입 (가장 권장)
- 필드 주입
- Setter 주입
- 일반 메서드 주입
우선 이 네 가지가 어떤 방식인지 설명한 뒤,
아래에서 기술적으로 왜 생성자 주입만 정답인지 파헤친다.
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의 핵심 처리기이며, 다음 순서로 동작한다.
- 빈의 생성자 목록 확인
- 주입할 후보 빈 탐색
- 생성자 우선순위 결정
- 필요한 빈 인스턴스 검색
- 생성자 인자 주입
- BeanPostProcessor 후처리 실행
즉, @Autowired는 BeanPostProcessor 중 하나로 동작한다.
3-2. “타입 → 이름 → Qualifier → Primary” 순서로 후보를 좁힌다
Autowired의 후보 선정 알고리즘:
- 타입으로 먼저 빈 목록 조회
List<BeanDefinition> candidates = findBeansOfType(type); - 타입 중복 시 이름으로 재검색
- 파라미터 이름 또는 필드 이름을 기준
- @Qualifier가 있으면 가장 높은 우선순위
- @Primary 지정된 빈이 우선 채택
3-3. 생성자가 여러 개 있을 때 스프링의 선택 기준
스프링은 다음 기준으로 “주입할 생성자”를 선택한다.
- @Autowired가 붙은 생성자
- 생성자가 한 개라면 자동으로 선택
- 생성자가 여러 개 + @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” 조합이 스프링 개발의 표준이다.
- 스프링의 자동 주입 알고리즘을 이해하면 유지보수, 확장, 디버깅 능력이 확연히 올라간다.