−알림을 보내는 코드가 메일 전송기를 직접 만들면, 메일 대신 문자 메시지를 보내고 싶을 때 알림 코드까지 고치게 된다. 전송기를 바깥에서 만들어 전달하면 알림 코드는 같은 호출 약속을 사용하는 채로 남고, 어떤 전송기를 연결할지는 프로그램을 조립하는 쪽에서 정할 수 있다. 필요한 협력 객체를 외부에서 연결하는 이 방법을 의존성 주입(dependency injection, DI)이라고 한다.[각주 1]
연결 방법만 정해서는 충분하지 않다. 같은 전송기를 여러 요청이 공유할지, 요청마다 새로 만들지, 연결을 누가 닫을지도 정해야 한다. 의존성 주입과 객체 수명은 서로 다른 질문이지만, 실제 프로그램을 조립할 때 함께 만나게 된다.
프레임워크의 호출 관계에서 객체 조립으로
Johnson과 Foote의 1988년 Designing Reusable Classes는 프레임워크가 큰 실행 흐름을 맡고 애플리케이션이 제공한 메서드를 호출하는 관계를 제어의 역전(inversion of control, IoC)이라고 설명했다. 라이브러리를 호출하는 프로그램과, 프로그램의 일부를 호출하는 프레임워크를 구별한 것이다.[각주 2]
1996년 Robert C. Martin의 의존성 역전 원칙(dependency inversion principle, DIP)은 소스 코드의 의존 방향을 다룬다. 업무 정책과 구체 구현이 추상화를 향하도록 설계해, 하위 구현의 변경 때문에 상위 정책이 함께 바뀌는 일을 줄이려는 원칙이다. 생성자에서 객체를 받더라도 매개변수 타입이 구체적인 메일 전송기라면, 정책 코드에는 여전히 그 구현에 대한 의존성이 남는다.[각주 3]
Martin Fowler는 2004년 1월 23일 글에서 당시 Java의 경량 컨테이너들이 하던 객체 연결을 살펴보고, 넓게 쓰이던 IoC라는 이름 대신 DI라는 구체적인 이름으로 설명했다. 글에는 생성자·setter·인터페이스를 통한 주입과 Service Locator의 차이가 함께 나온다. 이 날짜는 해당 글의 발표 시점이며, 협력 객체를 바깥에서 전달하는 기법 전체의 발명일로 삼을 수는 없다.[각주 1]
세 용어가 묻는 내용은 조금씩 다르다. IoC는 누가 실행 흐름을 호출하는지, DI는 누가 협력 객체를 연결하는지, DIP는 정책과 구현 사이의 소스 의존성이 어디를 향하는지에 초점을 둔다.
왜 전송기를 바깥에서 전달하는가
다음 예제는 알림 문장을 만드는 일과 전송기를 고르는 일을 나눈다. 실제 메일 서버 대신 문자열을 반환하는 전송기를 사용하므로, 네트워크 없이 어떤 구현이 호출됐는지 확인할 수 있다.
[SVG 다이어그램]
+개요
의존성 주입(dependency injection, DI)은 어떤 일을 수행하는 객체나 함수에 필요한 협력자를 바깥에서 연결하는 구성 방식이다. 알림을 보내는 코드는 전송기를 사용하고, 프로그램을 조립하는 코드는 메일 전송기와 문자 전송기 중 무엇을 연결할지 정한다. 이렇게 사용과 구성을 나누면 알림 문장을 만드는 로직을 그대로 두고 전송 수단을 바꿀 수 있다.
여기서 의존성은 다른 코드가 일을 끝내기 위해 사용하는 대상이다. 데이터베이스 접근 객체, 시계, 파일 저장소도 의존성이다. 주입은 이 대상을 사용하는 코드가 내부에서 직접 만드는 대신 바깥의 조립 코드가 넘겨주는 관계를 가리킨다. 생성자의 인수나 함수의 매개변수로 협력자를 전달하는 작은 프로그램에서도 이 관계를 볼 수 있다.
프로그램에는 서로 다른 종류의 변경이 생긴다. 할인 정책의 변경은 업무 로직을 바꾸지만, 같은 정책에 테스트용 저장소를 연결하는 일은 프로그램의 구성만 바꾼다. 두 변경을 한곳에서 처리하면 테스트를 실행하려고 실제 DB를 준비하거나 전송 수단을 바꾸려고 업무 코드까지 수정하게 된다. DI는 그 연결 지점을 밖으로 드러내는 방법이다.
의존성 주입을 읽을 때는 무엇을 사용할지, 누가 연결할지, 연결한 대상은 언제까지 살아 있을지를 살펴본다. 앞의 두 질문은 구성 관계를, 마지막 질문은 객체의 수명을 다룬다.
연혁
1988년: Ralph Johnson과 Brian Foote가 「Designing Reusable Classes」에서 재사용 프레임워크가 애플리케이션별 동작을 부르는 관계를 제어의 역전(IoC)으로 설명했다.[각주 1]
1996년 5월: Robert C. Martin이 C++ Report에 실은 「The Dependency Inversion Principle」에서 소스 코드의 의존 방향을 다루는 의존성 역전 원칙(DIP)을 정리했다.[각주 2]
2004년 1월 23일: Martin Fowler가 「Inversion of Control Containers and the Dependency Injection pattern」에서 PicoContainer와 Spring 같은 Java 경량 컨테이너의 객체 연결 방식을 Dependency Injection이라는 이름으로 설명하고 Service Locator와 비교했다. IoC 옹호자들과 논의한 명명과 비교가 이 글에 기록돼 있다.[각주 3]
1988년의 프레임워크 논의는 호출의 주도권을, 1996년의 DIP는 소스 의존 방향을, 2004년의 DI 설명은 객체 구성을 다룬다. 서로 이어지는 논의지만 질문이 다르다.
제어의 역전과 프레임워크
1988년 Ralph E. Johnson과 Brian Foote는 재사용 프레임워크의 호출 관계를 다뤘다. 라이브러리를 쓸 때는 애플리케이션이 필요한 함수를 호출하는데, 프레임워크에서는 재사용 가능한 실행 구조가 애플리케이션별 동작을 부른다. Johnson과 Foote가 제어의 역전이라고 부른 것이 이 뒤집힌 호출 관계다. 이들의 관심사는 프레임워크의 재사용이었고, 실행의 뼈대를 유지하면서 어떤 동작은 상속으로 바꾸고 어떤 부분은 교체 가능한 객체로 연결할지가 설계 문제로 드러났다.[각주 1]
이때의 IoC는 오늘날 DI 컨테이너가 하는 일보다 뜻이 넓어서, 이벤트 루프가 콜백을 부르는 관계에도 적용된다. 객체를 외부에서 받아 연결하는 문제는 이 넓은 호출 관계 안에서 따로 좁혀 설명해야 할 주제였다.
의존 방향과 DIP
Robert C. Martin이 1996년 5월 정리한 DIP는 소스 코드의 의존 방향을 다룬다. 상위 정책과 하위 구현이 모두 추상화를 향하게 해서, 구현을 바꿀 때 정책 코드까지 곧장 바뀌는 일을 줄이려는 원칙이다.[각주 2] 호출의 주도권을 묻던 IoC 논의에 더해, 여기서는 정책이 구체적인 장치나 저장소의 타입까지 알아야 하는지가 문제가 된다.
DI가 어떤 협력자를 전달하는지를 묻는다면, DIP는 전달받는 코드가 어떤 타입에 의존하는지를 묻는다. 생성자에서 구체적인 메일 전송기를 받아도 객체를 외부에서 전달하는 구성은 성립하고, 그때 정책과 구체 타입 사이의 의존 방향이 어떻게 정리됐는지는 따로 따져야 한다.
주입 방식과 Service Locator
Martin Fowler가 2004년 1월 23일 발표한 글은 여러 팀의 컴포넌트를 묶을 때 사용하는 코드에 구성의 세부 사항을 얼마나 남길 것인가를 다뤘다. 그는 넓은 IoC라는 말로는 무엇이 역전됐는지 분명하지 않다고 보고, IoC 옹호자들과 논의한 끝에 이 객체 연결 방식을 Dependency Injection이라는 구체적인 이름으로 설명했다.[각주 3]
생성자 주입은 객체를 만들 때 협력자를 받고, setter 주입은 생성한 뒤에 연결하며, 인터페이스 주입은 연결을 받는 동작 자체를 인터페이스로 드러낸다. 이 구분은 특정 컨테이너의 문법보다 구성과 사용이 어디에서 일어나는지를 기준으로 삼았다.
Fowler는 DI를 Service Locator와도 비교했다. Locator에서는 소비자가 서비스 저장소에 필요한 객체를 요청하고, DI에서는 협력자가 인수로 들어온다. 두 방식 모두 구성을 교체할 여지를 주지만 의존성을 발견하는 위치가 다르다.[각주 3] 인수로 받는 약속은 소비자의 선언에서 읽을 수 있는 반면, Locator 조회는 본문을 따라가야 보인다. Fowler가 강조한 구성과 사용의 분리로는 직접 조립하는 작은 프로그램과 컨테이너를 쓰는 프로그램을 함께 설명할 수 있다.
지금의 DI: 수명과 조립
컨테이너는 객체를 연결하면서 생성과 공유도 맡기 때문에, 요즘 DI 설명에는 transient·scoped·singleton 같은 수명 구분이 함께 나온다. .NET 컨테이너에서 transient는 컨테이너에서 꺼낼(resolve) 때마다 새로 만들고, scoped는 scope 안에서, singleton은 컨테이너 안에서 인스턴스 하나를 공유한다. 웹 앱에서는 보통 클라이언트 요청 하나가 scope의 경계가 된다.[각주 4]
Spring도 singleton을 컨테이너와 bean 정의를 기준으로 설명하며, prototype은 컨테이너에 요청할 때마다 새 인스턴스를 내준다. 다만 singleton을 만들 때 prototype을 한 번 주입하면 의존성은 그때 한 번만 해석되므로, singleton은 그때 받은 참조를 이후에도 계속 들고 있다.[각주 5]
수명 구분이 들어오면 DI에서 따질 것은 어떤 구현을 연결하느냐에 더해, 연결한 상태를 얼마나 오래 붙잡느냐가 된다. 오래 사는 객체가 짧은 요청의 상태를 보관하거나 파일·연결을 누가 정리할지 놓치면, 객체를 구성한 방식과 실제 실행 수명이 어긋난다.
다형성은 공통 호출 약속으로 여러 구현을 사용하는 관계다. DI는 그중 무엇을 연결할지를 소비자 밖의 조립 코드가 정하게 하는 구성 방식인데, 다형성이 없어도 구체 타입을 그대로 전달하면 DI다. 컨테이너는 이 조립을 수행하는 도구이고, 작은 프로그램에서는 평범한 함수 호출로도 같은 구성 관계를 드러낼 수 있다.
작은 예제
다음 예제는 알림을 사용하는 코드와 전송기를 고르는 코드를 나눈다. 전송 결과를 문자열로 돌려주므로 네트워크 없이 어느 객체가 연결됐는지 확인할 수 있다.
[SVG 다이어그램]