객체가 필요로 하는 의존 대상을 외부에서 넣어 주는 것(Dependency Injection).
세 가지 방법
① 생성자 주입 — 권장
@Service
public class OrderService {
private final PaymentClient client; // final 가능
public OrderService(PaymentClient client) { // @Autowired 생략 가능
this.client = client;
}
}
② 필드 주입 — 지양
@Autowired private PaymentClient client;
③ 수정자(setter) 주입 — 선택적 의존에만
@Autowired public void setClient(PaymentClient c) { this.client = c; }
왜 생성자 주입인가 — 이유가 네 개
① 불변으로 만들 수 있다
private final PaymentClient client; // 생성자 주입에서만 final 가능
주입 후 바뀌지 않음이 보장된다. 필드 주입은 final을 못 붙인다.
② 순환 참조를 기동 시점에 잡는다
- A가 B를 필요로 하고, B가 A를 필요로 한다
- 생성자 주입: 애플리케이션이 아예 기동하지 않는다 (즉시 발견)
- 필드 주입: 기동은 되고, 나중에 런타임에 이상 동작 ← 훨씬 나쁘다 "빨리 실패하는 것이 낫다" 는 원칙이 적용된 예다.
③ 테스트가 쉽다
// 생성자 주입 — 그냥 new 하면 된다
var service = new OrderService(new FakePaymentClient());
// 필드 주입 — 스프링 컨테이너를 띄우거나 리플렉션을 써야 한다
ReflectionTestUtils.setField(service, "client", fake); // 지저분하다
④ 의존성이 많아지면 눈에 보인다
public OrderService(A a, B b, C c, D d, E e, F f, G g) { }
// 생성자가 흉해진다 → "이 클래스가 너무 많은 일을 하는구나" 라는 신호
필드 주입은 어노테이션을 하나 더 붙이면 그만이라 비대해지는 것을 눈치채기 어렵다. 생성자의 불편함이 설계 냄새를 드러내는 장치로 작동한다.
스프링 4.3+ — @Autowired 생략
생성자가 하나뿐이면 @Autowired를 붙이지 않아도 자동 주입된다.
Lombok과 함께 쓰면 더 간결해진다.
@Service
@RequiredArgsConstructor // final 필드로 생성자를 만들어 준다
public class OrderService {
private final PaymentClient client;
private final OrderRepository repository;
}
같은 타입의 빈이 여럿일 때
public interface PaymentClient { }
@Component class TossClient implements PaymentClient { }
@Component class KakaoClient implements PaymentClient { }
// → NoUniqueBeanDefinitionException
해결 방법
@Primary // ① 기본으로 쓸 것을 지정
@Component class TossClient …
@Qualifier("kakaoClient") // ② 이름으로 지정
OrderService(PaymentClient client)
// ③ 전부 주입받아 직접 고른다 — 전략 패턴에 유용
OrderService(Map<String, PaymentClient> clients)
OrderService(List<PaymentClient> clients)
③은 결제 수단이 늘어날 때 기존 코드를 안 고쳐도 되는 구조를 만들 수 있어 실무에서 유용하다.
순환 참조가 생겼다면
스프링 2.6부터는 기본적으로 기동이 실패한다.
spring.main.allow-circular-references=true 로 켤 수 있지만,
그것은 문제를 미루는 것이다.
대개는 설계 문제의 신호다.
- A ⇄ B
- 공통 로직을 C로 추출해 A→C, B→C 로 바꾼다
- 또는 이벤트(ApplicationEventPublisher)로 방향을 끊는다