C# Dependency Injection: 실무 적용 가이드와 패턴 정리
c# dependency injection의 개념과 ASP.NET Core 실무 적용 방법을 코드 예제와 체크리스트로 정리합니다. 서비스 등록, 생명주기 관리, 주의점까지 바로 적용 가능한 실용 가이드입니다.
c# dependency injection이 필요한 상황
c# dependency injection은 코드의 결합도를 낮추고 테스트 가능성을 높이며, 런타임 구성 변경을 쉽게 합니다. 이 글에서는 개념 설명부터 ASP.NET Core에서의 서비스 등록, 생명주기 관리, 실무 예제와 체크리스트, 운영·보안 주의점까지 실무 웹 개발자가 바로 적용할 수 있도록 정리합니다.
c# dependency injection 핵심 개념
의존성 주입(DI)은 객체가 직접 의존성을 생성하지 않고 외부에서 주입받는 설계 패턴입니다. 주요 이점은 다음과 같습니다.
- 느슨한 결합: 구현체 교체가 쉬움
- 테스트 용이성: 목(Mock) 주입으로 단위 테스트 작성 쉬움
- 구성 분리: 설정과 구현을 분리하여 환경별 구성 가능
주요 방법: 생성자 주입(Constructor Injection), 속성 주입(Property Injection), 메서드 주입(Method Injection). 실무에서는 명시적이고 불변성을 보장하는 생성자 주입을 권장합니다.
c# dependency injection 실무 예제
다음은 ASP.NET Core에서 서비스 등록과 생성자 주입을 사용하는 기본 예제입니다.
// Program.cs (minimal API 또는 WebHostBuilder 환경) var builder = WebApplication.CreateBuilder(args); // 서비스 등록 builder.Services.AddScoped<IUserRepository, UserRepository>(); builder.Services.AddSingleton<IAppConfig, AppConfig>(); builder.Services.AddTransient<IEmailSender, SmtpEmailSender>(); var app = builder.Build(); app.MapGet("/users/{id}", async (int id, IUserRepository repo) => { var user = await repo.GetByIdAsync(id); return user is null ? Results.NotFound() : Results.Ok(user); }); app.Run(); // 서비스 구현 예 public interface IUserRepository { Task<User> GetByIdAsync(int id); } public class UserRepository : IUserRepository { private readonly IDbConnection _db; public UserRepository(IDbConnection db) { _db = db; } public Task<User> GetByIdAsync(int id) => /* DB access */ Task.FromResult(new User()); }
위 예제에서 핵심은 서비스 등록 방식(AddScoped/AddSingleton/AddTransient)과 생성자/파라미터 주입입니다. Controller나 Minimal API 핸들러에 필요한 의존성을 주입하면 프레임워크가 인스턴스를 해결합니다.
언제 쓰면 좋고 언제 피해야 할까
-
언제 쓰면 좋은가:
- 모듈 간 결합도를 낮추고 변경을 쉽게 하고 싶을 때
- 단위 테스트를 작성해야 할 때(목 주입이 필요할 때)
- 구성이나 구현체를 환경별로 바꿔야 할 때
-
언제 피해야 하는가:
- 간단한 유틸리티나 상태가 전혀 없는 정적 헬퍼에 DI를 적용하면 오히려 복잡도가 증가할 때
- DI 컨테이너 사용 목적이 애매한 경우(단순한 애플리케이션에 과도하게 도입)
- 성능이 매우 민감한 영역에서 불필요한 추상화가 오버헤드를 유발할 때
비교 기준표: 서비스 생명주기 선택 가이드
| 구분 | 항목1 | 항목2 | 항목3 |
|---|---|---|---|
| 내용 | Singleton | Scoped | Transient |
| 설명 | 앱 전체에서 하나만 존재, 상태 유지 가능 | 요청(혹은 범위) 당 인스턴스, 웹 요청에 적합 | 호출할 때마다 새 인스턴스, 가벼운 상태 없음 |
| 주의점 | 상태 관리로 인한 동시성 문제 가능 | 비동기/스레드 컨텍스트 주의 | 빈번한 생성으로 GC 부담 가능 |
실무 체크리스트
- 서비스 등록
- 모든 서비스는 인터페이스와 구현을 분리했는가?
- 필요한 생명주기를 정확히 선택했는가(Singleton/Scoped/Transient)?
- 외부 리소스(DB, HttpClient)는 적절히 팩토리 또는 클라이언트 팩으로 관리하는가?
- 테스트
- 유닛 테스트에서 목(Mock)을 쉽게 주입할 수 있는가?
- 통합 테스트를 위한 TestHost 설정을 분리했는가?
- 구성
- 환경별 설정(AppSettings, Secret)은 외부 구성으로 분리했는가?
- DI 컨테이너에 직접 구현체를 하드코딩하지 않았는가?
- 운영
- Singleton 객체의 상태 동기화가 필요한가 확인했는가?
- 라이프사이클로 인한 자원 누수(Disposable) 처리를 했는가?
성능·보안·운영 주의점
성능
- Singleton으로 많은 리소스를 보관하면 메모리 증가와 동시성 이슈가 발생할 수 있습니다.
- Transient를 과도하게 사용하면 객체 생성 비용과 GC 부담이 커질 수 있습니다.
보안
- 서비스에 민감한 정보를 직접 보관하지 마세요. 설정은 구성 시스템(Secret Manager, Azure Key Vault 등)을 사용하세요.
- DI로 주입되는 HttpClient 등 외부 연결 자원은 재사용 하도록 HttpClientFactory 사용을 권장합니다.
운영
- IDisposable 구현체는 컨테이너가 해제하도록 등록하여 누수를 방지하세요.
- 앱 재시작 시 Singleton의 상태 의존성을 최소화하세요(무상태 설계 권장).
구현 판단 기준표
| 구분 | 항목1 | 항목2 | 항목3 |
|---|---|---|---|
| 내용 | 적용 권장 | 주의 적용 | 비권장 |
| 사용예 | 비즈니스 로직 서비스 | DB 커넥션 래퍼 | 단순 유틸리티(static) |
c# dependency injection 실무 팁 (패턴과 도구)
- 팩토리 패턴과 결합: 복잡한 생성 로직은 팩토리를 통해 캡슐화하고 팩토리를 DI로 주입하세요.
- 옵션 패턴: IOptions<T>로 구성 바인딩을 명확히 하세요.
- Scrutor 같은 라이브러리로 어셈블리 스캔을 통한 자동 등록을 활용하면 코드가 간결해집니다.
자주 묻는 질문
Q1: DI로 모든 클래스를 인터페이스로 분리해야 하나요? A1: 모든 클래스를 인터페이스로 만들 필요는 없습니다. 외부에 교체 가능성이나 테스트 필요성이 높은 경계(Boundary)에 해당하는 서비스에 인터페이스를 도입하세요.
Q2: Singleton으로 등록한 서비스에서 상태를 유지해도 되나요? A2: 가능하지만 동시성 문제와 메모리 누수에 주의해야 합니다. 멀티스레드 환경에서는 불변(immutable) 객체나 동기화 처리를 고려하세요.
Q3: HttpClient를 DI로 주입할 때 주의할 점은? A3: HttpClient를 직접 Singleton으로 주입하면 DNS 변경 등의 문제를 겪을 수 있습니다. IHttpClientFactory를 사용해 재사용 가능한 클라이언트를 생성하세요.
Q4: Scoped와 Transient를 섞어 사용할 때 주의점은? A4: Scoped 서비스 내부에서 Transient를 사용하면 일반적으로 문제없지만, Singleton 내부에서 Scoped를 직접 주입하려 하면 서비스 해석 시점 문제(존재하지 않는 스코프)가 발생합니다. 이 경우 IServiceProvider나 팩토리를 사용하세요.
정리
- c# dependency injection은 결합도 감소, 테스트 용이성, 구성 분리를 제공한다.
- ASP.NET Core에서는 AddSingleton/AddScoped/AddTransient으로 생명주기를 관리해야 한다.
- 생성자 주입을 기본으로 사용하고, 팩토리/옵션 패턴을 병행하면 실무에서 유리하다.
- 성능과 보안 고려: Singleton 상태 관리, IDisposable 처리, HttpClientFactory 사용을 검토하라.
- 체크리스트를 통해 서비스 등록, 테스트, 구성, 운영 항목을 점검하라.