−1. 프레임워크에서 먼저 드러난 제어의 역전
라이브러리를 사용하는 프로그램은 보통 자기 실행 흐름 안에서 라이브러리 함수를 호출한다. 프레임워크에서는 프레임워크가 실행의 뼈대를 제공하고, 사용자가 등록하거나 재정의한 코드를 호출한다. 이벤트 루프가 버튼의 클릭 처리기를 부르는 관계를 떠올리면 된다.
Ralph Johnson과 Brian Foote는 1988년 「Designing Reusable Classes」에서 이 호출 관계를 제어의 역전(inversion of control, IoC)으로 설명했다. 관심사는 재사용 가능한 프레임워크였다. 애플리케이션이 모든 절차를 직접 지휘하는 대신, 정해진 확장 지점에 자신의 동작을 제공하는 구조를 정리한 것이다.[각주 1]
객체를 누가 연결하느냐는 이보다 좁은 문제다. 프레임워크의 호출 흐름을 따르면서 객체를 직접 만들 수도 있고, 프레임워크 없이 평범한 함수 호출만으로 협력자를 전달할 수도 있다. 이 차이 때문에 IoC만으로 객체 조립 방식을 설명하면 정확히 무엇이 역전됐는지 추가 설명이 필요했다.
2. 1996년의 의존성 역전과 2004년의 명명
Robert C. Martin의 1996년 의존성 역전 원칙(dependency inversion principle, DIP)은 소스 코드의 의존 방향을 다뤘다. 상위 정책과 하위의 구체적인 구현이 추상화를 향하도록 해, 구현의 변화가 정책 코드에 직접 번지는 일을 줄이려는 원칙이다.[각주 2]
Martin Fowler는 2004년 1월 23일 글에서 PicoContainer와 Spring 같은 Java 경량 컨테이너의 객체 연결 방식을 살펴보고, 이 동작을 DI라는 더 구체적인 이름으로 설명했다. 당시의 문제는 여러 팀이 만든 컴포넌트를 어떻게 연결하고, 설정을 바꾸면서도 사용하는 코드를 유지할 것인가였다. 그 글은 생성자·setter·인터페이스 주입과 Service Locator를 함께 비교한다.[각주 3]
따라서 세 용어는 서로 연결되면서도 초점이 다르다. IoC는 호출 흐름의 주도권, DIP는 소스 의존 방향, DI는 협력자를 제공하는 방식을 묻는다. 생성자에 구체적인 전송기 타입을 받으면 DI를 사용하면서도 그 구체 타입에 대한 소스 의존성은 남는다.
Fowler의 2004년 글은 넓은 IoC라는 말 안에 있던 객체 구성 문제를 별도의 이름으로 정리한 기록이다. 협력 객체를 외부에서 전달하는 기존의 구성 방식이 당시 경량 컨테이너의 문제와 어떻게 연결됐는지 보여준다.
3. 주입하는 위치에 따른 구분
생성자 주입은 객체를 만들 때 필수 협력자를 전달한다. 생성된 객체가 정상적으로 동작하려면 무엇이 필요한지 생성자에 나타나며, 생성 이후 의존성을 고정하는 설계와도 잘 맞는다.
setter나 속성 주입은 생성 이후에 대상을 연결한다. 선택적인 기능이나 프레임워크의 구성 과정에 사용할 수 있지만, 생성과 사용 사이에 연결이 끝났는지 확인해야 한다. 인터페이스 주입은 의존성을 받는 동작 자체를 별도의 인터페이스로 표현한다. 함수나 메서드의 인수로 필요한 대상을 받는 방식도 같은 구성 원리를 적용한다.
어떤 방식을 쓰든 연결을 받는 코드에는 자신이 사용하는 약속이 남는다. DI를 적용했다는 말만으로 모든 모듈이 독립적이 되지는 않는다. 실제로 교체하고 싶은 대상에 맞춰 인터페이스와 책임을 정해야 한다.
4. 작은 예제: 전송기를 고르는 곳
다음 예제의 목적은 전송기의 선택과 알림의 사용이 서로 다른 위치에 있다는 점을 보는 것이다. 네트워크 전송 대신 문자열을 돌려주므로 어느 구현이 호출됐는지 바로 확인할 수 있다.
+재사용 프레임워크가 드러낸 호출 관계 (1988)
의존성 주입의 배경을 살펴볼 출발점 중 하나는 라이브러리와 프레임워크를 구별하던 객체지향 연구다. 라이브러리에서는 애플리케이션이 필요한 함수를 호출하지만, 프레임워크에서는 재사용 가능한 실행 구조가 애플리케이션별 동작을 부른다.
Ralph Johnson과 Brian Foote의 1988년 「Designing Reusable Classes」는 이 관계를 제어의 역전, IoC로 설명했다. 관심사는 프레임워크의 재사용이었다. 실행의 뼈대를 유지하면서 어떤 동작은 상속으로 바꾸고 어떤 부분은 교체 가능한 객체로 연결할 것인지가 설계의 문제로 드러났다.[각주 1]
이때의 IoC는 오늘날 DI 컨테이너보다 넓은 뜻이다. 이벤트 루프가 콜백을 부르는 관계에도 적용된다. 객체를 외부에서 받아 연결하는 문제는 이 넓은 호출 관계 안에서 더 구체적으로 설명해야 할 주제였다.
정책과 구현의 의존 방향을 묻다 (1996)
Robert C. Martin의 1996년 의존성 역전 원칙, DIP는 소스 코드의 의존 방향을 다뤘다. 상위 정책과 하위 구현이 추상화를 향하도록 해서, 구현 변경이 정책 코드의 변경으로 곧장 번지는 일을 줄이려는 원칙이다.[각주 2]
호출의 주도권을 묻던 문제와 함께, 정책이 구체적인 장치나 저장소의 타입까지 알아야 하는지가 중요해진다. DI가 어떤 협력자를 전달하는가를 묻는다면, DIP는 전달받는 코드가 어떤 타입에 의존하는가를 묻는다.
생성자에서 구체적인 메일 전송기를 받아도 객체를 외부에서 전달하는 구성은 가능하다. 그 상태에서 정책과 구체 타입의 의존 방향이 어떻게 정리됐는지는 별도로 살펴봐야 한다.
경량 컨테이너의 객체 연결에 이름을 붙이다 (2004)
Martin Fowler의 2004년 1월 23일 글은 PicoContainer와 Spring 같은 Java 경량 컨테이너를 배경으로 한다. 여러 팀의 컴포넌트를 묶을 때 사용하는 코드에 구성의 세부 사항을 얼마나 남길 것인가가 당시 글의 문제였다.
Fowler는 넓은 IoC라는 말로는 무엇이 역전됐는지 분명하지 않다며, IoC 옹호자들과의 논의를 거쳐 이 객체 연결 방식을 Dependency Injection이라는 구체적인 이름으로 설명했다. 이 글은 명명과 비교의 기록이며, 협력자를 외부에서 전달한 모든 코드의 최초 시점을 정하는 자료는 아니다.[각주 3]
생성자 주입은 객체를 만들 때 협력자를 받고, setter 주입은 생성 뒤 연결한다. 인터페이스 주입은 연결을 받는 동작 자체를 인터페이스로 드러낸다. 이 구분의 중심은 특정 컨테이너의 문법보다 구성과 사용의 위치에 있었다.
같은 시기에 비교된 Service Locator (2004)
같은 글은 Service Locator도 비교한다. Locator에서는 소비자가 서비스 저장소에 필요한 객체를 요청한다. DI에서는 협력자가 인수로 들어온다. 두 방식 모두 구성을 바꿀 수 있지만 의존성을 발견하는 위치가 다르다.[각주 3]
이 비교가 남긴 기준은 구성과 사용의 분리다. 인수의 약속은 소비자의 선언에서 읽을 수 있고, Locator 조회는 본문을 따라가야 보인다. 직접 조립하는 작은 프로그램과 컨테이너를 사용하는 프로그램을 같은 기준으로 설명할 수 있다.
객체 연결에서 공유 범위와 정리 책임으로
컨테이너는 객체를 연결하면서 생성과 공유도 맡는다. 이 때문에 현대의 DI 설명에는 transient·scoped·singleton 같은 수명 구분이 함께 등장한다. .NET의 컨테이너에서는 각각 요청 시 생성, scope 안의 공유, 컨테이너 안의 공유를 뜻한다. 웹 요청은 scope의 흔한 경계다.[각주 4]
Spring도 singleton을 컨테이너와 bean 정의를 기준으로 설명하며, prototype은 요청할 때 새 인스턴스를 제공한다. singleton을 생성할 때 prototype을 한 번 주입하면 그 참조는 이후에도 유지된다.[각주 5]
이 단계에서 문제는 구현 교체뿐 아니라 연결한 상태를 얼마나 오래 붙잡는가로 넓어진다. 짧은 요청의 상태를 장기 객체가 보관하거나 파일·연결의 정리 책임을 놓치면 객체 구성과 실행 수명이 어긋난다.
오늘날의 DI를 읽는 좌표
1988년의 프레임워크 논의, 1996년의 의존 방향, 2004년의 객체 구성은 서로 연결되면서도 다른 질문을 다룬다. 이 흐름을 구분하면 IoC·DIP·DI를 도구 하나의 별칭으로 합치지 않고 읽을 수 있다.
다형성은 공통 호출 약속으로 여러 구현을 사용하는 관계다. DI는 그중 무엇을 현재 소비자에게 연결할지를 정한다. 컨테이너는 이 조립을 수행하는 도구이며, 작은 프로그램에서는 평범한 함수 호출로 같은 구성 관계를 드러낼 수 있다.
현재의 구성 관계를 보여주는 작은 예제
다음 예제는 알림을 사용하는 코드와 전송기를 고르는 코드를 나눈다. 전송 결과를 문자열로 돌려주므로 네트워크 없이 어느 객체가 연결됐는지 확인할 수 있다.