목차
제어의 역전(IoC) / 의존성 주입(DI) 은 객체 지향 프로그래밍에서 오브젝트의 생명주기와 의존관계를 외부에서 관리하는 프로그래밍 모델을 말한다.
1. IOC
Inversion of Control이라는 용어로 제어의 역전이라고 한다.
개발자가 객체의 생성 또는 생명주기를 직접 제어하는 하지 않고 외부에서 객체를 생성하고 제어 및 관리하여 객체간의 의존성을 해결하한다. 즉, 제어권한을 다른 대상에게 위임하는 것을 말한다. (Bean을 관리해주는 Container)
이를 통해 객체 간의 결합도를 낮추고 유연성과 확장성을 높일 수 있다.
스프링 프레임워크 에서는 IOC Container가 존재하며 ApplicationContext와 Bean Factory라는 핵심 컨테이너가 있다. Bean을 등록/생성/조회 하고 그 외 빈 관리하는 기능을 담당한다.
이런 IOC Container는 Bean 객체를 관리한다.
BeanFactory
-
Bean 등록/생성/조회/반환을 관리
-
BeanFactory를 바로 사용하지 않고, 이를 확장한 ApplicationContext를 사용
-
getBean() 메서드가 정의되어 있다.
ApplicationContext
-
Bean 등록/생성/조회/반환을 관리 (=BeanFactory)
-
Spring의 각종 부가 서비스를 추가로 제공
-
Spring이 제공하는 ApplicationContext 구현 클래스가 여러 가지 종류가 있다.(StaticApplicationContext, GenericXmlApplicationContext, WebApplicationContext,XmlWebApplicationContext)
2. DI
Dependency Injection라는 용어로 의존관계 주입이라고 한다.
스프링에서 지원하는 IoC의 한 형태로 객체 간의 의존 관계를 외부(IOC)에서 생성하여 주입하는 방식으로 객체 간 의존성을 문제를 해결해줄 수 있다. 스프링에서는 Bean 설정 정보로 바탕으로 컨테이너가 자동으로 연결해준다.
Spring에서 DI의 주입 방법
- Constructor Injection : 생성자 삽입
- 스프링에서 가장 권장하는 방법이다.
- 4.3이상 버전부터 @Autowired의 생략가능
private final CustomerRepository customerRepository;
@Autowired
public CustomerServiceImpl(CustomerRepository customerRepository) {
this.customerRepository = customerRepository;
}
- Field Injection : 멤버 변수 삽입
- 작성은 짧지만 생성자에서 필수 의존성이 드러나지 않아 단위 테스트와 불변성 유지가 어렵다. 순환 의존성은 필드 주입만의 문제가 아니며 설계에서 풀어야 한다.
@Autowired
private CustomerRepository customerRepository;
- Method(Setter) Injection : 메소드 매개 변수 삽입
- 생성할 때 의존성을 주입하지 않고, 원할 때 함수를 호출하여 의존성 주입을 할 수 있다.
private CustomerRepository customerRepository;
public void setCustomerRepository(CustomerRepository customerRepository) {
this.customerRepository = customerRepository;
}
주문 서비스로 흐름 확인하기
DI를 볼 때 “컨테이너가 알아서 넣어 준다”에서 멈추면 실제 실패를 이해하기 어렵다. 주문 서비스가 저장소를 필요로 하는 경우를 보자. 서비스는 어떤 저장소 구현을 쓸지 직접 결정하지 않고 생성자에 필요한 타입을 적는다.
public interface OrderRepository {
void save(Order order);
}
@Service
public class OrderService {
private final OrderRepository repository;
public OrderService(OrderRepository repository) {
this.repository = repository;
}
public void place(Order order) {
if (order == null) throw new IllegalArgumentException("order required");
repository.save(order);
}
}
애플리케이션 시작 시 컨테이너는 OrderService 빈 정의를 읽고 OrderRepository 타입의 빈을 찾는다. 구현 빈이 하나면 주입해 서비스를 만든다. 구현이 전혀 없으면 시작 단계에서 실패한다. 두 개라면 @Qualifier 또는 @Primary와 같은 선택 규칙을 명확히 해야 한다. 이 실패는 실행 중 임의의 주문 요청에서 NullPointerException이 터지는 것보다 찾기 쉽다.
flowchart LR A["컴포넌트 탐색 / @Bean 설정"] --> B["빈 정의 등록"] B --> C["OrderRepository 후보 찾기"] C --> D["OrderService 생성자 호출"] D --> E["요청 시 place 호출"]
그림의 생성 단계와 요청 처리 단계를 분리해서 생각한다. 생성자 주입은 서비스 생성 전에 의존성이 충족되는지 확인한다. OrderService가 싱글턴 빈이라면 여러 요청이 같은 인스턴스를 사용할 수 있으므로 요청별 가변 상태를 필드에 저장하지 않는다. 의존하는 저장소 구현도 공유 사용에 적합한지 확인한다.
테스트와 선택 기준
컨테이너 없이 단위 테스트를 할 때는 OrderRepository를 구현한 작은 가짜 객체를 생성자에 전달하면 된다. 저장 호출 여부와 전달된 주문을 확인하면 외부 DB 연결 없이 서비스의 규칙을 테스트할 수 있다. Spring 통합 테스트에서는 빈 등록과 실제 저장소 설정을 검사한다. 두 테스트는 목적이 다르다.
선택적 기능이라면 필드에 @Autowired(required=false)를 붙이고 이후에 null 검사하는 방식보다, Optional<T> 또는 별도의 기본 구현을 쓰는 편이 계약을 드러내기 쉽다. 순환 의존성이 발견되면 @Lazy로 임시 우회하기 전에 두 서비스가 서로 어떤 책임을 요구하는지 분리한다. 두 클래스가 서로를 생성해야 한다면 생성 순서를 바꾸는 것만으로는 설계 문제가 해결되지 않는다.
참고: Spring 컨테이너 개요, @Autowired 문서, 빈 스코프.