블로그로 돌아가기
C#

C# Dependency Injection: 실무 적용 가이드와 패턴 정리

c# dependency injection의 개념과 ASP.NET Core 실무 적용 방법을 코드 예제와 체크리스트로 정리합니다. 서비스 등록, 생명주기 관리, 주의점까지 바로 적용 가능한 실용 가이드입니다.

DependencyInjectionASP.NET CoreConstructorInjectionServiceLifetimeDI-pattern
C# dependency injection을 설명하는 코드와 다이어그램이 포함된 기술 블로그 썸네일

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
내용SingletonScopedTransient
설명앱 전체에서 하나만 존재, 상태 유지 가능요청(혹은 범위) 당 인스턴스, 웹 요청에 적합호출할 때마다 새 인스턴스, 가벼운 상태 없음
주의점상태 관리로 인한 동시성 문제 가능비동기/스레드 컨텍스트 주의빈번한 생성으로 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 사용을 검토하라.
  • 체크리스트를 통해 서비스 등록, 테스트, 구성, 운영 항목을 점검하라.