# Dependency Injection (의존 주입) 의존 주입(Dependency Injection, DI)은 객체지향 프로그래밍에서 객체 간의 의존 관계를 외부에서 주입해 주는 설계 패턴이다. IoC(Inversion of Control)의 한 구현으로, 클라이언트가 직접 의존 객체를 생성하지 않고, 외부 컨테이너(Container)나 팩토리(Factory)를 통해 의존 객체를 주입받도록 한다. 이를 통해 컴포넌트 간 결합도를 낮추고, 테스트 용이성과 유지보수성을 높일 수 있다. --- ## 정의 DI는 "의존성 주입"으로 번역되며, 객체가 필요로 하는 의존 객체를 외부에서 제공해 주입하는 방식이다. 주입은 생성자(Constructor Injection), 세터(Setter Injection), 인터페이스 주입(Interface Injection) 등의 방법으로 이루어질 수 있으며, 일반적으로 IoC 컨테이너 또는 서비스 제공자(Provider)가 이를 관리한다. 주요 용어 - Client: 의존 객체를 사용하는 주체 - Dependency: Client가 필요로 하는 객체 - Dependency Injection Container / IoC Container: 의존 객체의 생성, 구성, 주입을 관리하는 프레임워크 또는 구성요소 - Provider / Factory: 의존 객체의 생성 로직을 추상화한 구성 요소 - Wiring / Configuration: 의존 관계를 연결하는 구성 --- ## 배경과 역사 DI는 전통적으로 제어의 역전(Inversion of Control, IoC) 원칙의 구현 방식 중 하나로 자리매김했다. 2000년대 이후 프레임워크(Spring, .NET Core 등)의 등장으로 DI 컨테이너가 널리 보급되었고, 애플리케이션의 모듈화와 테스트 가능성을 크게 향상시켰다. DI의 핵심 아이디어는 "의존성 생성 로직을 비즈니스 로직에서 분리"하는 것이며, 이를 통해 구현 세부사항의 변경이 비즈니스 로직에 미치는 영향을 최소화한다. --- ## 핵심 개념 - 의존성(Dependency): 컴포넌트가 동작하기 위해 필요한 다른 컴포넌트 - 주입(Injection): 외부 컨테이너가 의존성을 제공하는 행위 - IoC 컨테이너(IoC Container): 의존성 생성, 설정, 주입, 스코프 관리 등을 수행하는 프레임워크 - 공급자(Provider) / 팩토리(Factory): 의존성의 생성 로직을 제공하는 구성요소 - 스코프(Scope): 의존성의 인스턴스 수명 주기(예: Singleton, Prototype/Transient, Request/Session 등) DI의 기본 흐름 1. 애플리케이션 시작 시 IoC 컨테이너가 구성 정보를 읽고 의존성 그래프를 구성한다. 2. 클라이언트가 의존성을 필요로 하면 컨테이너가 주입한다. 3. 컨테이너는 의존성의 생애 주기를 관리한다(예: 싱글톤으로 한 번만 생성하거나 매 주입마다 새 객체를 생성). 주요 주입 방식 - Constructor Injection: 의존성을 생성자 매개변수로 주입 - Setter Injection: 의존성을 setter 메서드를 통해 주입 - Interface Injection: 인터페이스를 통해 의존성을 주입하는 방식(현대 프레임워크에서 일반적으로 사용되지는 않음) --- ## DI의 유형 - Constructor Injection: 불변성(immutable)과 명확한 의존성 계약을 확보하기에 가장 일반적이고 안전한 방식이다. - Setter Injection: 선택적 의존성이나 순환 의존성 해결에 사용될 수 있다. 다만 초기화 상태가 완전하지 않을 수 있다. - Field Injection: 리플렉션 기반으로 주입하는 방법으로 테스트 편의성은 좋지 않고 권장되지 않는 경우가 많다. - Interface Injection: 인터페이스가 주입 행위를 정의하는 방식이나, 현재는 특정 프레임워크에서 제한적으로 사용된다. --- ## 작동 원리와 예시 다음 예시는 Java를 사용한 간단한 DI 구현 방식과 비교 예이다. - 비DI(직접 의존성 생성) ```java // 비DI 예시 public class Engine { public void start() { /* ... */ } } public class Car { private final Engine engine; public Car() { // 직접 의존성 생성 this.engine = new Engine(); } public void drive() { engine.start(); } } ``` - DI(Constructor Injection) ```java // DI 예시(생성자 주입) public class Engine { public void start() { /* ... */ } } public class Car { private final Engine engine; public Car(Engine engine) { this.engine = engine; } public void drive() { engine.start(); } } // 간단한 컨테이너 역할을 하는 예 public class SimpleContainer { public static void main(String[] args) { Engine engine = new Engine(); Car car = new Car(engine); // 의존성 주입 car.drive(); } } ``` - DI 컨테이너 활용 예(스프링 스타일) ```java // 의존성 구성(구성 파일 또는 어노테이션 기반) @Configuration public class AppConfig { @Bean public Engine engine() { return new Engine(); } @Bean public Car car(Engine engine) { return new Car(engine); } } // 컨테이너 사용 public class Application { public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); Car car = context.getBean(Car.class); car.drive(); context.close(); } } ``` --- ## 구현 전략과 도구 - IoC 컨테이너의 선택 - 자바: Spring Framework, Dagger, Guice - .NET 계열: Microsoft.Extensions.DependencyInjection, Autofac, Ninject - JavaScript/TypeScript: InversifyJS, tsyringe - Kotlin: Koin, Dagger/Hilt - 라이프사이클 관리 - Singleton: 애플리케이션 전체에서 하나의 인스턴스를 공유 - Prototype/Transient: 주입 시마다 새로운 인스턴스 생성 - Scoped/Request: 웹 어플리케이션에서 요청 단위의 인스턴스 관리 - Provider/Factory 패턴 - 의존성 생성을 복잡한 로직으로 추상화하고, 컨테이너의 주입은 더욱 단순화 - 구성 방식 - 어노테이션 기반: @Autowired, @Inject, @Bean 등으로 의존성 정의 - 코드 기반 구성: 코드로 의존성 그래프를 구성하는 방식(프로그램적 컨테이너) --- ## 장점 - 결합도 감소: 클라이언트와 의존 객체 간의 직접적인 생성 의존성 제거 - 테스트 용이성: 의존 객체를 모킹(Mock)이나 스텁(Stubs)으로 대체 가능 - 유연성 증가: 구현체 교체 시 클라이언트 코드를 변경할 필요가 적음 - 재사용성과 구성성 향상: 의존 객체를 재사용 가능한 컴포넌트로 분리 --- ## 단점 및 주의점 - 초기 설정 복잡도 증가: 컨테이너 구성과 모듈화 필요 - 디버깅이 어려워질 수 있음: 런타임 의존성 해결 흐름이 내부적으로 작동 - 과도한 추상화 위험: 과도한 DI로 코드가 난해해질 수 있음 - 성능 오버헤드: 주입 과정에서 약간의 런타임 오버헤드가 발생 가능 - 순환 의존성 관리 필요: 서로를 의존하는 순환 구조는 주입 실패의 원인 --- ## 실무 적용 가이드 - 언제 DI를 도입할지 판단 - 애플리케이션의 모듈 간 결합도가 높고, 단위 테스트가 빈번한 경우 DI 도입을 고려 - 객체 생성 로직이 복잡하거나 구현이 자주 변경될 가능성이 있을 때 유리 - 모듈 경계의 명확화 - 인터페이스를 통한 의존성 계약 정의 - 구현체의 민감한 의존성은 외부로부터 주입받도록 설계 - 인터페이스 설계 원칙 - DIP(Dependency Inversion Principle)에 따라 high-level 모듈이 low-level 모듈에 의존하지 않도록 추상화 - 테스트 전략 - 단위 테스트에서 DI 컨테이너의 런타임 의존성 대신 명시적 주입 사용 - Mockito, Fake, Stub 등의 테스트 더블 활용 - 성능과 모니터링 - 컨테이너 초기화 시점, 의존 그래프의 크기, 스코프 영향 등을 모니터링 - 점진적 도입 - 핵심 비즈니스 로직부터 DI 도입, 점차 서비스 계층, 컨트롤러/애플리케이션 레이어로 확장 --- ## 테스트 전략 예시 - 단위 테스트에서 의존성 주입 확인 ```java // 의존성 주입이 정상적으로 이루어졌는지 확인하는 간단한 예 public class ClientTest { @Test void testDoWork_withMockedDependency() { Service mockService = Mockito.mock(Service.class); Client client = new Client(mockService); client.doWork(); Mockito.verify(mockService).execute(); } } ``` - 통합 테스트에서 컨테이너를 통한 주입 확인 ```java // Spring-like 통합 테스트 예시 (가상의 어노테이션 사용) @SpringBootTest class IntegrationTest { @Autowired private Car car; @Test void testCarOperation() { assertNotNull(car); car.drive(); } } ``` --- ## 용어 해설 - Dependency: 의존성, 클라이언트가 동작하기 위해 필요로 하는 객체 - Injection: 주입, 외부 컨테이너가 의존성을 제공하는 행위 - IoC: 제어의 역전, 의존성 생성과 관리의 책임을 애플리케이션 코드가 아닌 프레임워크에 위임 - IoC Container: 의존성 생성, 구성, 주입을 관리하는 프레임워크 - Scope: 의존성 인스턴스의 생명 주기(싱글톤, 트랜지언트, 스코프드 등) - Provider/Factory: 의존성의 생성 로직을 캡슐화한 구성요소 - DIP: 의존 관계를 추상화에 의존시키고 구체 구현에 의존하지 않도록 하는 원칙 --- ## 요약 Dependency Injection은 의존성의 생성과 구성을 프레임워크 또는 컨테이너에 위임함으로써 코드의 결합도를 낮추고, 테스트 및 유지보수성을 향상시키는 설계 패턴이다. 다양한 주입 방식과 라이프사이클 관리 전략을 활용해 애플리케이션의 모듈화와 확장성을 높일 수 있다. --- 관련 문서: [[Inversion of Control]], [[Dependency Inversion Principle]], [[Service Locator pattern]]