의존성 주입
개요
의존성 주입(dependency injection, DI)은 어떤 일을 수행하는 객체나 함수에 필요한 협력자를 바깥에서 연결하는 구성 방식이다. 알림을 보내는 코드는 전송기를 사용하고, 프로그램을 조립하는 코드는 메일 전송기와 문자 전송기 중 무엇을 연결할지 정한다. 이렇게 사용과 구성을 나누면 알림 문장을 만드는 로직을 그대로 두고 전송 수단을 바꿀 수 있다.
여기서 의존성은 다른 코드가 일을 끝내기 위해 사용하는 대상이다. 데이터베이스 접근 객체, 시계, 파일 저장소도 의존성이 된다. 주입이라는 말은 특별한 런타임 기술을 가리키기보다, 사용자가 내부에서 직접 만들던 대상을 외부의 조립 코드가 제공한다는 관계를 설명한다. 생성자의 인수나 함수의 매개변수로 전달하는 작은 프로그램에서도 이 관계를 볼 수 있다.
이 구분이 필요한 이유는 프로그램에 서로 다른 종류의 변경이 생기기 때문이다. 할인 정책의 변경은 업무 로직을 바꾸지만, 같은 정책에 테스트용 저장소를 연결하는 일은 프로그램의 구성만 바꾼다. 두 변경을 한곳에서 처리하면 테스트를 실행하려고 실제 DB를 준비하거나 전송 수단을 바꾸려고 업무 코드까지 수정하게 된다. DI는 그 연결 지점을 밖으로 드러내는 방법이다.
의존성 주입을 이해할 때는 세 질문을 이어서 보면 좋다. 무엇을 사용할 것인지, 누가 그것을 연결하는지, 연결한 대상은 언제까지 살아 있는지다. 앞의 두 질문은 구성 관계를, 마지막 질문은 객체의 수명을 다룬다.
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. 작은 예제: 전송기를 고르는 곳
다음 예제의 목적은 전송기의 선택과 알림의 사용이 서로 다른 위치에 있다는 점을 보는 것이다. 네트워크 전송 대신 문자열을 돌려주므로 어느 구현이 호출됐는지 바로 확인할 수 있다.
type Sender = { send(message: string): string };
function createNotifier(sender: Sender) {
return {
notify(name: string) {
return sender.send(`Hello, ${name}`);
},
};
}
const mail: Sender = { send: message => `mail:${message}` };
const sms: Sender = { send: message => `sms:${message}` };
console.log(createNotifier(mail).notify('Ada'));
console.log(createNotifier(sms).notify('Ada'));
출력은 mail:Hello, Ada와 sms:Hello, Ada다. createNotifier를 호출하는 쪽이 협력 객체를 선택하고, notify는 이미 전달받은 sender.send를 호출한다. 두 객체의 공통 호출 약속을 타입으로 표현했으며, 이 조립에는 DI 컨테이너가 등장하지 않는다.
5. 직접 조립, 컨테이너, Service Locator
직접 조립하는 프로그램에서는 시작 지점이나 기능의 경계에서 생성자를 호출하고 객체를 연결한다. 연결이 많아지면 컨테이너가 등록 정보에 따라 객체를 만들고 필요한 대상을 찾아 전달할 수 있다. DI는 구성 관계이고, 컨테이너는 그 구성을 수행하는 도구다.
Service Locator에서는 사용하는 코드가 저장소나 컨테이너에 원하는 서비스를 요청한다. DI에서는 협력자가 외부에서 들어오므로 인수에서 의존성이 드러나지만, Locator를 사용하면 본문 안의 조회를 읽어야 실제 의존성을 알 수 있다. Fowler가 두 방식을 비교할 때도 이 구성과 사용의 위치가 중요한 쟁점이었다.3
6. 객체 수명과 자원 소유권
같은 협력 객체를 여러 번 연결할지, 연결할 때마다 새로 만들지는 별도의 선택이다. .NET 컨테이너의 transient는 요청할 때마다 생성하고, scoped는 같은 scope 안에서 공유하며, singleton은 해당 컨테이너에서 공유하는 수명으로 설명된다. 웹 요청은 scope를 만드는 흔한 계기다.4
이 이름은 제품의 계약과 함께 읽어야 한다. Spring의 singleton도 컨테이너와 bean 정의를 기준으로 하며, prototype은 요청할 때 새 인스턴스를 제공한다. 한 singleton이 생성될 때 prototype을 주입받았다면 그 의존성은 그 시점에 한 번 연결된다.5
오랫동안 살아 있는 객체가 짧은 수명의 상태를 붙잡으면, 요청 사이에 사용자의 상태를 잘못 공유하거나 이미 정리한 자원을 다시 사용할 수 있다. 가비지 컬렉션이 객체 메모리를 회수하는 일과 파일·연결을 닫는 책임도 구별된다. 그래서 객체 구성에는 생성뿐 아니라 공유 범위와 정리 책임까지 따라온다.
7. 다른 개념과의 관계
다형성은 사용하는 코드가 같은 호출 약속으로 여러 구현을 다룰 수 있게 한다. DI는 그 구현 중 무엇을 해당 코드에 연결할지 정한다. 다형성을 읽은 뒤 이 관계를 보면, 호출 계약과 객체 구성을 나누어 이해할 수 있다.
전략 패턴은 교체 가능한 동작을 표현하고 DI는 그 전략을 전달하는 데 쓰일 수 있다. 한편 DI의 대상에는 시계나 저장소처럼 전략이라는 이름을 붙이지 않는 협력자도 포함된다. 개념의 범위는 구체적인 프레임워크의 등록 문법보다 넓다.
각주
- Ralph E. Johnson·Brian Foote, 「Designing Reusable Classes」, JOOP 1(2), 1988, pp.22–35, “White-box vs Black-box Frameworks”. 저자 공개본. ↩
- Robert C. Martin, 「The Dependency Inversion Principle」, 1996. 저자 자료. ↩
- Martin Fowler, 「Inversion of Control Containers and the Dependency Injection pattern」, 2004-01-23. 원문. ↩
- Microsoft, “Service lifetimes”. 공식 문서. ↩
- Spring Framework, “Bean Scopes”. 공식 문서. ↩