소프트웨어 설계에서 종속성 주입(Dependency Injection, DI)은 서로 강하게 결합된(종속적인) 객체를 느슨하게 분리된(독립적인) 구조로 전환하고, 필요한 의존 객체를 외부에서 주입하는 기법을 말합니다. DI를 적용하면 클래스 간 결합도가 낮아져 유연하고 테스트하기 쉬운 애플리케이션을 만들 수 있습니다.
종속성 주입의 4가지 유형
C#에서 활용할 수 있는 대표적인 DI 방식은 다음과 같습니다.
- 생성자 주입(Constructor Injection)
- 세터 주입(Setter Injection)
- 인터페이스 기반 주입(Interface-based Injection)
- 서비스 로케이터 주입(Service Locator Injection)
1. 생성자 주입(Constructor Injection)
매개변수가 있는 생성자(parameterized constructor)를 통해 인터페이스 타입의 매개변수를 외부에 노출하는 방식입니다. 객체가 생성되는 시점에 다른 클래스에 대한 종속성이 생성자 메서드를 통해 함께 주입됩니다. 가장 널리 사용되는 방식으로, .NET의 내장 DI 컨테이너(Microsoft.Extensions.DependencyInjection) 역시 기본적으로 이 방식을 지원합니다.
public class OrderService
{
private readonly INotifier _notifier;
public OrderService(INotifier notifier)
{
_notifier = notifier; // 생성자를 통한 종속성 주입
}
}2. 세터 주입(Setter Injection)
공개 속성(public property)의 get, set 접근자를 활용해 종속성을 주입하는 방식입니다. 객체 생성 이후에도 의존 객체를 교체할 수 있어 필수적이지 않은 선택적(optional) 종속성에 특히 적합합니다.
public class ReportService
{
public ILogger Logger { get; set; } // 속성을 통한 종속성 주입
}3. 인터페이스 주입(Interface Injection)
세터 주입과 유사하지만, 일반적인 getter/setter 대신 종속성을 설정하는 멤버를 정의한 별도의 인터페이스를 사용한다는 점이 다릅니다. 즉, 명시적인 계약(contract)을 통해 종속성을 주입하므로 주입 규칙이 더 엄격하게 강제되고, 구현 클래스가 반드시 해당 인터페이스를 따르도록 만들 수 있습니다.
4. 서비스 로케이터 주입(Service Locator Injection)
'종속성 흡수(Dependency Absorption)'라고도 불리는 이 방식은 new 연산자를 직접 사용하는 것을 대체합니다. 객체를 직접 생성하지 않고 로케이터(컨테이너)에 요청하여 필요한 서비스를 가져오기 때문에, 클래스 간 종속성이 코드 밖으로 숨겨집니다. 다만 의존 관계가 코드에 드러나지 않아 가독성과 추적성이 떨어질 수 있어, 최근에는 생성자 주입에 비해 사용을 지양하는 추세입니다.
종속성 주입의 장점
- 클래스 간 결합도 감소 — 구현체가 아닌 추상화(인터페이스)에 의존하게 되어 변경에 유연합니다.
- 코드 재사용성 향상 — 동일한 컴포넌트를 다양한 상황에서 손쉽게 재활용할 수 있습니다.
- 유지보수성 개선 — 변경의 영향 범위가 줄어들어 수정과 확장이 쉬워집니다.
- 테스트 용이성 향상 — 실제 객체 대신 목(Mock) 객체를 주입해 단위 테스트를 간편하게 수행할 수 있습니다.
- 중앙화된 구성 관리 — DI 컨테이너를 통해 애플리케이션 전체의 의존 관계를 한곳에서 관리할 수 있습니다.
종속성 주입의 단점
DI의 가장 큰 단점은 인스턴스 수와 해결해야 할 종속성이 지나치게 많아질 경우, 여러 객체를 함께 구성하고 관리하는 작업이 매우 복잡해진다는 점입니다. 또한 초기 학습 비용이 발생하고, 잘못 설계된 DI 구성은 컴파일 시점이 아닌 런타임에 오류로 드러날 수 있습니다. 따라서 프로젝트의 규모와 성격에 맞게 DI 컨테이너와 주입 방식을 신중하게 선택하는 것이 중요합니다.