스프링은 다양한 의존관계 주입 방법을 지원한다. 하지만 실무에서는 특정 방식을 표준으로 사용하며, 여러 개의 빈이 조회될 때 발생하는 충돌을 우아하게 해결하는 것이 중요하다. 이번 글에서는 주입 방식의 종류와 권장되는 선택지, 그리고 빈 충돌 해결 전략을 알아본다.
1. 의존관계 주입의 4가지 방법
의존관계 주입은 크게 생성자 주입, 수정자(Setter) 주입, 필드 주입, 일반 메서드 주입으로 나뉜다.
- 생성자 주입: 객체 생성 시점에 딱 한 번만 호출되며, 불변성과 필수 의존관계에 사용된다.
- 수정자(Setter) 주입: 선택적이거나 변경 가능성이 있는 의존관계에 사용된다.
- 필드 주입: 코드가 간결하지만 외부에서 변경이 불가능하여 테스트하기 어렵다는 치명적인 단점이 있다. 실무에서는 사용하지 않는 것을 권장한다.
- 일반 메서드 주입: 한 번에 여러 필드를 주입받을 수 있으나 거의 사용되지 않는다.
2. 생성자 주입을 선택해야 하는 이유
과거에는 수정자나 필드 주입을 많이 사용했으나, 최근 스프링을 포함한 대부분의 DI 프레임워크는 생성자 주입을 권장한다. 그 이유는 다음과 같다.
- 불변(Immutability): 대부분의 의존관계는 애플리케이션 종료 시점까지 변경될 일이 없다. 생성자 주입은 객체 생성 시점에 주입이 완료되므로 변경 가능성을 원천 차단한다.
- 누락 방지: 순수 자바 코드로 단위 테스트를 수행할 때, 의존관계 주입이 누락되면 컴파일 오류가 발생한다. 이는 런타임 에러보다 훨씬 안전하다.
- final 키워드 사용: 생성자 주입에서만 필드에 final 키워드를 사용할 수 있다. 이는 생성자에서 값이 설정되지 않는 오류를 컴파일 시점에 막아준다.
3. 롬복(Lombok)과 최신 트렌드
실무에서는 생성자 주입을 사용하면서도 코드를 간결하게 유지하기 위해 롬복 라이브러리의 @RequiredArgsConstructor를 주로 활용한다.
@Component
@RequiredArgsConstructor // final이 붙은 필드를 모아 생성자를 자동 생성한다.
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
}
이 방식은 @Autowired를 생략(생성자가 1개인 경우 자동 주입)하면서도 필수 의존관계를 명확히 관리할 수 있어 가장 선호되는 방식이다.
4. 조회 빈이 2개 이상일 때의 문제 해결
@Autowired는 타입으로 빈을 조회한다. 만약 DiscountPolicy 인터페이스의 구현체인 FixDiscountPolicy와 RateDiscountPolicy가 모두 스프링 빈으로 등록되어 있다면, 스프링은 어떤 빈을 주입해야 할지 몰라 NoUniqueBeanDefinitionException을 발생시킨다. 이를 해결하기 위한 세 가지 방법은 다음과 같다.
4.1. @Autowired 필드 명칭 매칭
스프링은 타입 매칭을 시도한 후, 여러 빈이 있으면 필드 이름이나 파라미터 이름으로 빈 이름을 추가 매칭한다.
@Autowired
private DiscountPolicy rateDiscountPolicy // 필드명을 빈 이름과 일치시킴
4.2. @Qualifier 사용
추가 구분자를 붙여주는 방식이다. 빈 등록 시와 주입 시에 각각 어노테이션을 명시한다.
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}
// 주입 시
public OrderServiceImpl(MemberRepository repository, @Qualifier("mainDiscountPolicy") DiscountPolicy policy) { ... }
4.3. @Primary 사용 (가장 권장)
우선순위를 정하는 방법이다. 여러 빈이 매칭되면 @Primary가 붙은 빈이 우선권을 가진다.
@Component
@Primary // 기본 할인 정책으로 설정
public class RateDiscountPolicy implements DiscountPolicy {}
우선순위: 상세하게 동작하는 @Qualifier가 기본값처럼 동작하는 @Primary보다 우선권이 높다.
5. 조회한 빈이 모두 필요할 때: 전략 패턴의 구현
의도적으로 해당 타입의 모든 빈이 필요한 경우도 있다. 예를 들어 고객이 할인 종류를 선택할 수 있는 경우, 스프링의 Map이나 List 주입을 통해 전략 패턴을 간단히 구현할 수 있다.
public class DiscountService {
private final Map<String, DiscountPolicy> policyMap;
public DiscountService(Map<String, DiscountPolicy> policyMap) {
this.policyMap = policyMap; // 모든 DiscountPolicy 구현체가 Map에 담겨 주입된다.
}
public int discount(Member member, int price, String code) {
DiscountPolicy policy = policyMap.get(code); // 코드를 통해 동적으로 전략 선택
return policy.discount(member, price);
}
}
6. 자동 vs 수동 등록 기준
최근 스프링은 자동 빈 등록(@Component)을 기본으로 사용하는 추세다. 하지만 기준은 필요하다.
- 업무 로직 빈: 숫자도 많고 비즈니스 요구사항에 따라 빈번하게 추가/변경되므로 자동 등록을 사용한다. (Controller, Service, Repository 등)
- 기술 지원 빈: 공통 관심사(AOP), DB 연결 등 기술적인 문제를 다루는 객체는 수가 적고 영향 범위가 넓으므로 수동 등록을 통해 설정 정보에 명확히 드러내는 것이 유지보수에 유리하다.
- 다형성을 적극 활용하는 비즈니스 로직: 위에서 살펴본 DiscountService처럼 여러 구현체가 주입되는 경우, 한눈에 파악할 수 있도록 수동 등록하거나 특정 패키지에 모아두어야 한다.
'Spring > Core' 카테고리의 다른 글
| [Advanced-1] 예제 만들기 (0) | 2025.12.30 |
|---|---|
| [Basic-8] 빈 생명주기 콜백과 빈 스코프의 활용 (0) | 2025.12.30 |
| [Basic-6] 컴포넌트 스캔과 자동 의존관계 주입 (0) | 2025.12.30 |
| [Basic-5] 싱글톤 컨테이너와 CGLIB의 동작 원리 (0) | 2025.12.30 |
| [Basic-4] 스프링 컨테이너의 생성과 빈 관리 메커니즘 (0) | 2025.12.30 |
