객체의 생성과 생명주기 관리를 개발자가 아닌 컨테이너가 가져가는 것 (Inversion of Control, 제어의 역전).
무엇이 "역전"되었나
// 전통적 방식 — 내가 제어한다
class OrderService {
private PaymentClient client = new TossPaymentClient(); // 내가 만든다
}
// IoC — 컨테이너가 제어한다
class OrderService {
private final PaymentClient client;
OrderService(PaymentClient client) { this.client = client; } // 받는다
}
"필요한 것을 내가 만든다" → "누군가 만들어서 준다" 로 방향이 뒤집혔다.
왜 이것이 중요한가
첫 번째 코드의 문제를 보자.
- ① 구현체가 코드에 박혀 있다
- 카카오페이로 바꾸려면 OrderService 를 수정해야 한다
- ② 테스트할 수 없다
- 실제 결제가 호출된다. 가짜로 바꿔 끼울 방법이 없다
- ③ 생명주기를 제어할 수 없다
- 매번 새로 만든다. 공유·풀링을 적용하려면 코드를 고쳐야 한다
두 번째는 셋 다 해결된다. 의존 대상을 밖에서 결정할 수 있기 때문이다.
프레임워크와 라이브러리의 차이 — 같은 개념
| 라이브러리 | : 내 코드가 라이브러리를 부른다 | (내가 제어) |
|---|---|---|
| 프레임워크 | : 프레임워크가 내 코드를 부른다 | (제어 역전) |
- "Don't call us, we'll call you" (할리우드 원칙)
스프링에서 컨트롤러 메서드를 만들면 내가 부르지 않는다. 디스패처 서블릿이 요청이 오면 대신 불러 준다. 이것도 IoC다.
스프링 컨테이너
ApplicationContext
├─ 빈 정의를 읽는다 (@Component, @Bean, XML)
├─ 객체를 생성한다
├─ 의존 관계를 주입한다
├─ 초기화 콜백 (@PostConstruct)
├─ 사용
└─ 소멸 콜백 (@PreDestroy)
빈(Bean) 은 컨테이너가 관리하는 객체다. 등록되지 않은 객체는
@Transactional도 @Cacheable도 동작하지 않는다 — AOP 프록시를
컨테이너가 씌워 주기 때문이다. new로 만든 객체에 어노테이션이 안 먹는
이유가 이것이다.
DI와의 관계 — 자주 혼동된다
IoC : 제어를 넘긴다는 넓은 개념 (원칙)
- DI — : 그 구현 방법 중 하나 — 의존성을 주입한다 (기법)
IoC ⊃ DI
IoC를 구현하는 다른 방법으로 서비스 로케이터가 있다 (필요할 때 컨테이너에 물어보는 방식). DI가 더 나은 이유는 의존 관계가 생성자 시그니처에 드러나 감춰지지 않기 때문이다.
얻는 것
| 이점 | 설명 |
|---|---|
| 결합도 감소 | 인터페이스에만 의존. 구현체 교체가 설정 문제가 된다 |
| 테스트 용이 | 가짜 구현을 주입해 단위 테스트 가능 |
| 관심사 분리 | 비즈니스 로직이 객체 생성·조립을 신경 쓰지 않는다 |
| 횡단 관심사 | 컨테이너가 프록시를 씌워 트랜잭션·로깅을 얹는다 |
면접 답변 골격
"객체 생성과 조립의 책임을 컨테이너에 넘기는 것입니다. 그 결과 구현체 교체가 코드 수정이 아니라 설정 변경이 되고, 테스트에서 가짜 구현을 끼울 수 있게 됩니다. DI는 IoC를 구현하는 방법 중 하나입니다."