IoC와 DI는 어디서 갈리고 스프링은 무엇을 맡나
IoC와 DI의 관계, 스프링 컨테이너가 대신하는 일, 생성자 주입을 권하는 이유를 실행 결과로 보여 줍니다.
IoC와 DI는 어디서 갈리나
의존성은 한 클래스가 일을 하려고 쓰는 다른 객체입니다(OrderService에게는 OrderRepository).
IoC(제어의 역전)는 내 코드가 라이브러리를 부르는 대신 프레임워크가 정해 둔 때에 내 코드를 부르는 구조입니다. Martin Fowler는 이를 할리우드 원칙 "전화하지 마세요, 우리가 연락합니다"로 설명합니다. 요청이 올 때 스프링 MVC가 내 컨트롤러 메서드를 부르는 것도 IoC입니다.
DI(의존성 주입)는 객체가 쓸 객체를 직접 new 하지 않고 생성자 같은 통로로 밖에서 받는 방식입니다. 의존성을 만드는 제어가 객체 밖으로 넘어가기 때문에 스프링 문서는 DI를 IoC의 특수한 형태라고 씁니다.
DI라는 이름도 이 구분에서 나왔습니다. Fowler는 2004년 글에서 IoC가 모든 프레임워크의 공통점이라 너무 넓다며 이 방식에 Dependency Injection이라는 좁은 이름을 붙였습니다.
직접 new 하는 코드와 주입받는 코드
같은 서비스를 두 방식으로 쓰면 이렇습니다.
// 직접 new: 어떤 저장소를 쓸지 OrderService가 정합니다class OrderService { private final OrderRepository repo = new JdbcOrderRepository();}// 주입받기: 생성자로 받기만 합니다class OrderService { private final OrderRepository repo; OrderService(OrderRepository repo) { this.repo = repo; }}직접 new 하면 어떤 저장소를 쓸지 OrderService가 정해서, 구현을 바꾸거나 테스트에서 가짜 저장소를 넣으려면 OrderService를 고쳐야 합니다. 주입받으면 OrderService는 인터페이스만 알고, 넘기는 객체만 바꾸면 됩니다.
스프링이 대신해 주는 일
주입받는 코드가 늘면 누군가는 객체를 순서대로 만들어 이어 줘야 합니다. 스프링에서는 IoC 컨테이너(ApplicationContext)가 이 일을 합니다. 컨테이너가 만들고 관리하는 객체를 빈이라고 부르며, @Component를 붙인 클래스가 빈이 됩니다.
컨테이너(ApplicationContext)가 @Component가 붙은 클래스 둘을 찾습니다.
OrderService 생성자가 OrderRepository를 원하니 컨테이너가 저장소를 먼저 만듭니다.
만든 저장소를 생성자에 넣어야 OrderService가 완성됩니다. 서비스는 넣어 준 것만 씁니다.
필드 주입 서비스를 직접 new 하면 채워 줄 컨테이너가 없어 필드가 빈 채로 남습니다.
생성자 주입, 필드 주입, 순환 참조(A와 B가 서로를 필요로 하는 상태)를 스프링 부트 없이 Spring Framework 7.0.9로 돌려 봤습니다.
OrderService 생성자가 OrderRepository를 원하니, 컨테이너가 저장소를 먼저 만들어 넣습니다.
생성자 주입은 가짜 저장소를 넘기면 바로 돌고, @Autowired 필드는 채워 줄 컨테이너가 없어 NullPointerException이 납니다.
A를 만들려면 B가, B를 만들려면 A가 먼저 있어야 해서 시작할 때 실패합니다.
같은 순환인데 필드 주입이면 그대로 뜹니다. 꼬인 설계를 모른 채 넘어갑니다.
생성자 주입을 권하는 이유
스프링 DI 문서는 필수 의존성은 생성자로, 기본값이 있는 선택 의존성은 세터로 받으라고 합니다. 생성자 주입이면 필드를 final로 두어 바꿀 수 없게 만들 수 있고, 의존성이 빠진 채로 객체가 만들어지지 않습니다.
| 생성자 | 세터 | 필드 | |
|---|---|---|---|
| 스프링 없이 new 해서 테스트 | 바로 됨 | 세터 호출 필요 | NPE |
| 순환 참조 | 시작 때 실패 | 통과 | 통과 |
| 스프링 문서가 권하는 곳 | 필수 의존성 | 선택 의존성 | 따로 없음 |
순환 참조에서 시작부터 실패하는 것은 설계가 꼬였다는 신호를 가장 일찍 받는 셈입니다. 세터·필드 주입은 한쪽이 덜 만들어진 채로 상대에게 넘어가는데, Spring Boot 2.6부터는 이것도 기본으로 막습니다(spring.main.allow-circular-references=true로 다시 허용).
자주 하는 오해 세 가지
"IoC와 DI는 같은 말이다"는 Fowler가 직접 짚은 혼동입니다. JUnit이 내 @Test 메서드를 부르는 것도 IoC지만, 메서드를 부르는 일 자체는 의존성 주입이 아닙니다.
"DI를 하려면 스프링이 필요하다"도 틀렸습니다. Stack Overflow 최고 득표 답변은 객체를 생성자에 직접 넘기는 것도 프레임워크만큼 좋은 주입이라고 씁니다. 스프링은 넘기는 일을 대신할 뿐입니다.
"@Autowired를 빼면 주입이 안 된다"는 생성자 주입에서는 생성자가 둘 이상일 때만 맞습니다. 위 실행에서도 생성자가 하나뿐인 OrderService는 애너테이션 없이 주입됐습니다(스프링 문서).
언제 무엇을 쓰나
반드시 있어야 하는 의존성은 생성자로 받고 필드를 final로 둡니다. 기본값이 있는 선택 의존성만 세터로 받습니다. 필드 주입은 스프링 테스트 클래스처럼 내가 new 할 일이 없는 곳에만 남겨 둡니다. 생성자 인자가 계속 늘어나면 주입 방식을 바꿀 때가 아니라 클래스를 나눌 때입니다.