2025년 12월 30일 작성
Dependency Injection - Spring의 의존성 주입
Dependency Injection(DI)은 객체 간의 의존 관계를 외부에서 주입하는 design pattern으로, Spring IoC container가 bean의 생성과 의존성 주입을 자동으로 관리합니다.
Dependency Injection
- Dependency Injection(DI)은 객체가 필요로 하는 의존성을 외부에서 주입받는 design pattern입니다.
- 객체가 직접 의존 객체를 생성하지 않고, 외부에서 생성된 객체를 전달받습니다.
- 객체 지향 programming에서 널리 사용되는 개념으로, Spring에만 국한되지 않습니다.
- Spring framework는 IoC(Inversion of Control) container를 통해 DI를 구현합니다.
- IoC container가 bean의 lifecycle과 의존 관계를 관리합니다.
- 개발자는 의존성 설정만 선언하고, 실제 주입은 container가 처리합니다.
DI가 필요한 이유
- 객체 간의 결합도를 낮추고 유연한 구조를 만들기 위해 DI를 사용합니다.
- 강한 결합은 변경에 취약하고, 느슨한 결합은 변경에 유연합니다.
강한 결합의 문제
- 객체 내부에서 다른 객체를 직접 생성하면 강한 결합이 발생합니다.
- 의존 객체가 변경되면 해당 객체를 사용하는 모든 code를 수정해야 합니다.
- test 시 mock 객체로 대체하기 어렵습니다.
public class OrderService {
// 강한 결합 : OrderService가 EmailSender를 직접 생성
private final EmailSender emailSender = new EmailSender();
public void placeOrder(Order order) {
// 주문 처리 logic
emailSender.send("주문이 완료되었습니다.");
}
}
EmailSender를SmsSender로 변경하려면OrderServicecode를 수정해야 합니다.OrderService를 test할 때 실제 email이 발송되는 문제가 있습니다.
느슨한 결합의 장점
- 외부에서 의존 객체를 주입받으면 느슨한 결합을 달성합니다.
- interface에 의존하므로 구현체 변경이 용이합니다.
- runtime에 의존 관계가 결정되어 유연한 구조를 가집니다.
public class OrderService {
// 느슨한 결합 : interface에 의존하고 외부에서 주입받음
private final MessageSender messageSender;
public OrderService(MessageSender messageSender) {
this.messageSender = messageSender;
}
public void placeOrder(Order order) {
// 주문 처리 logic
messageSender.send("주문이 완료되었습니다.");
}
}
MessageSenderinterface를 구현한 어떤 객체도 주입 가능합니다.- test 시 mock 객체를 주입하여 실제 전송 없이 검증할 수 있습니다.
- SOLID 원칙 중 OCP(Open-Closed Principle)와 DIP(Dependency Inversion Principle)를 준수합니다.
Spring IoC Container
- Spring IoC container는 bean의 생성, 설정, 조립을 담당하는 핵심 component입니다.
ApplicationContextinterface가 IoC container를 대표합니다.- container는 설정 metadata를 읽어 bean을 생성하고 의존성을 주입합니다.
flowchart LR
config[설정 Metadata]
container[Spring IoC Container]
beans[구성된 Bean들]
config --> container
container --> beans
- 설정 metadata는 XML, Java annotation, Java code 등으로 정의할 수 있습니다.
- 현대 Spring 개발에서는 annotation 기반 설정이 주로 사용됩니다.
Bean 등록 방식
- Component Scan :
@Component,@Service,@Repository,@Controllerannotation이 붙은 class를 자동으로 bean으로 등록합니다.
@Service
public class OrderService {
// Spring이 자동으로 bean으로 등록
}
- Java Config :
@Configurationclass에서@Beanmethod로 직접 bean을 정의합니다.
@Configuration
public class AppConfig {
@Bean
public OrderService orderService(MessageSender messageSender) {
return new OrderService(messageSender);
}
}
의존성 주입 시점
- Spring container는 bean 생성 시점에 의존성을 주입합니다.
- 생성자 주입 : bean 생성과 동시에 의존성 주입이 완료됩니다.
- Field/수정자 주입 : bean 생성 후 의존성이 주입됩니다.
- container는 의존 관계를 분석하여 올바른 순서로 bean을 생성합니다.
- A가 B에 의존하면, B를 먼저 생성한 후 A를 생성합니다.
- 순환 참조가 발생하면 application context 초기화에 실패합니다.
Spring의 의존성 주입 방식
- Spring은 생성자 주입, 필드 주입, 수정자 주입 세 가지 방식을 지원합니다.
- Spring team은 생성자 주입을 권장합니다.
| 방식 | 특징 | 권장 여부 |
|---|---|---|
| 생성자 주입 | 불변성 보장, 필수 의존성 명시, 순환 참조 조기 발견 | 권장 |
| Field 주입 | 간결한 code, test 어려움, final 불가 | 비권장 |
| 수정자 주입 | 선택적 의존성에 적합, final 불가 | 상황에 따라 |
생성자 주입
- 가장 권장되는 방식으로, 생성자를 통해 의존성을 주입받습니다.
- 의존성을
final로 선언하여 불변성을 보장합니다. - 필수 의존성이 누락되면 compile time에 오류가 발생합니다.
- 순환 참조 시 application 시작 시점에 오류가 발생하여 조기에 발견할 수 있습니다.
- 의존성을
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final MessageSender messageSender;
// 생성자가 하나면 @Autowired 생략 가능
public OrderService(OrderRepository orderRepository, MessageSender messageSender) {
this.orderRepository = orderRepository;
this.messageSender = messageSender;
}
}
- Lombok의
@RequiredArgsConstructor를 사용하면 더 간결하게 작성할 수 있습니다.
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final MessageSender messageSender;
}
Field 주입
- field에
@Autowired를 선언하여 직접 주입받는 방식입니다.- code가 간결하지만, test와 유지 보수에 불리합니다.
final선언이 불가능하여 불변성을 보장할 수 없습니다.- Spring container 없이는 의존성 주입이 불가능하여 unit test가 어렵습니다.
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private MessageSender messageSender;
}
- 실무에서는 비권장되며, 주로 test code나 legacy code에서 볼 수 있습니다.
수정자 주입
- setter method에
@Autowired를 선언하여 주입받는 방식입니다.- 선택적 의존성(optional dependency)에 적합합니다.
final선언이 불가능합니다.
@Service
public class OrderService {
private OrderRepository orderRepository;
private MessageSender messageSender;
@Autowired
public void setOrderRepository(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
@Autowired(required = false)
public void setMessageSender(MessageSender messageSender) {
this.messageSender = messageSender;
}
}
required = false로 설정하면 해당 bean이 없어도 application이 정상 시작됩니다.
동일 Type Bean의 선택
- 동일한 type의 bean이 여러 개 존재할 때, Spring은 어떤 bean을 주입할지 결정할 수 없습니다.
NoUniqueBeanDefinitionException이 발생하며 application context 초기화에 실패합니다.@Primary와@Qualifier로 이 문제를 해결합니다.
@Primary: bean 정의 시점에 기본 bean을 지정합니다.- 동일 type의 bean 중
@Qualifier없이 주입할 때 선택되는 기본값입니다. - 대부분의 경우 하나의 구현체를 사용하고, 특수한 경우에만 다른 구현체가 필요할 때 유용합니다.
- 동일 type의 bean 중
@Qualifier: 주입 시점에 특정 bean을 명시적으로 선택합니다.- bean 이름이나 custom qualifier로 원하는 bean을 지정합니다.
@Primary보다 우선순위가 높아,@Qualifier가 있으면@Primary설정을 override합니다.
@Component
@Primary
public class EmailSender implements MessageSender { }
@Component
public class SmsSender implements MessageSender { }
@Service
public class NotificationService {
// @Primary인 EmailSender가 주입됨
public NotificationService(MessageSender messageSender) { }
}
@Service
public class UrgentNotificationService {
// @Qualifier로 SmsSender 명시적 주입
public UrgentNotificationService(@Qualifier("smsSender") MessageSender messageSender) { }
}
Reference
- https://docs.spring.io/spring-framework/reference/core/beans/introduction.html
- https://docs.spring.io/spring-framework/reference/core/beans/dependencies/factory-collaborators.html