📌 .NET에서 Dependency Injection(DI) 기초 가이드

안녕하세요😊
오늘은 .NET에서 제공하는 의존성 주입(Dependency Injection, DI)에 대해 깊이 있게 다뤄보겠습니다.

DI는 현대적인 .NET 애플리케이션에서 유지보수성과 확장성을 높이는 핵심 개념입니다.
이 글을 끝까지 읽으시면 DI의 개념부터 고급 활용법까지 완벽히 이해하실 수 있을 거예요!


1. 의존성 주입(DI)이란?

🔹 1.1 의존성 문제와 DI의 필요성

개발을 하다 보면 클래스 간의 의존성(Dependency)이 강해지는 경우가 많습니다.
예를 들어, OrderService에서 PaymentService를 직접 생성하면 다음과 같은 문제가 발생합니다.

public class OrderService
{
    private readonly PaymentService _paymentService;

    public OrderService()
    {
        _paymentService = new PaymentService(); // 강한 결합 (Tightly Coupled)
    }
}

문제점

  • OrderService가 PaymentService의 구체적인 구현을 직접 알고 있어야 함
  • PaymentService의 변경이 OrderService에도 영향을 줌
  • 테스트가 어렵다! → PaymentService를 Mocking하기 어려움

이러한 문제를 해결하기 위해 의존성 주입(DI)이 등장했습니다.

🔹 1.2 DI 적용 후

DI를 적용하면 다음과 같이 코드를 개선할 수 있습니다.

public class OrderService
{
    private readonly IPaymentService _paymentService;

    public OrderService(IPaymentService paymentService) // 생성자 주입
    {
        _paymentService = paymentService;
    }
}

DI의 장점

객체 간 결합도 감소 → 변경이 유연해짐
Mocking을 쉽게 적용 가능단위 테스트(Unit Test) 가능
유지보수성과 확장성이 높아짐


2. .NET의 DI 컨테이너 사용법

🔹 2.1 기본 DI 컨테이너 설정

.NET에서는 내장된 DI 컨테이너(IServiceCollection)를 제공합니다.
서비스 등록은 Program.cs에서 아래처럼 하면 됩니다.

var builder = WebApplication.CreateBuilder(args);

// 서비스 등록
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.AddSingleton<IPaymentService, PaymentService>();

var app = builder.Build();
app.Run();

🔹 2.2 서비스 등록 방법

.NET DI 컨테이너에서는 세 가지 수명 주기(Lifetime)를 지원합니다.

수명 주기메서드특징

Transient AddTransient<TInterface, TImplementation>() 요청 시마다 새 인스턴스 생성
Scoped AddScoped<TInterface, TImplementation>() HTTP 요청당 하나의 인스턴스 유지
Singleton AddSingleton<TInterface, TImplementation>() 애플리케이션 종료 시까지 동일한 인스턴스 사용

수명 주기 예제

builder.Services.AddTransient<IProductService, ProductService>();  // 요청마다 새 객체
builder.Services.AddScoped<IUserService, UserService>();          // 요청 내에서 유지
builder.Services.AddSingleton<ILoggerService, LoggerService>();   // 앱 실행 동안 동일 객체

3. DI 컨테이너의 동작 원리

🔹 3.1 DI 컨테이너 내부 원리

DI 컨테이너는 다음 3단계 과정으로 동작합니다.

1️⃣ 서비스 등록 → IServiceCollection을 사용해 인터페이스-구현체를 등록
2️⃣ 객체 생성 → IServiceProvider가 의존성을 분석하여 객체를 생성
3️⃣ 주입(Injection) 실행 → 필요한 곳에 인스턴스를 전달

🔹 3.2 IServiceProvider 활용

IServiceProvider를 직접 활용해 수동으로 인스턴스를 생성할 수도 있습니다.

var serviceProvider = new ServiceCollection()
    .AddSingleton<ILoggerService, LoggerService>()
    .BuildServiceProvider();

var logger = serviceProvider.GetRequiredService<ILoggerService>();
logger.Log("DI 컨테이너 직접 사용");

 


4. DI를 활용한 패턴과 실전 예제

🔹 4.1 Factory 패턴과 DI 활용

Factory 패턴을 활용하면 동적으로 객체를 생성할 수 있습니다.

public interface ICar { void Drive(); }
public class BMW : ICar { public void Drive() => Console.WriteLine("BMW Driving..."); }

public class CarFactory
{
    private readonly IServiceProvider _serviceProvider;
    public CarFactory(IServiceProvider serviceProvider) => _serviceProvider = serviceProvider;

    public ICar CreateCar() => _serviceProvider.GetRequiredService<ICar>();
}

🔹 4.2 Middleware에서 DI 사용

public class LoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILoggerService _logger;

    public LoggingMiddleware(RequestDelegate next, ILoggerService logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task Invoke(HttpContext context)
    {
        _logger.Log("Request Started");
        await _next(context);
        _logger.Log("Request Ended");
    }
}

public void Configure(IApplicationBuilder app)
{
    app.UseMiddleware<LoggingMiddleware>();
}

🔹 4.3 HostedService에서 DI 사용

public class MyBackgroundService : BackgroundService
{
    private readonly ILogger<MyBackgroundService> _logger;
    public MyBackgroundService(ILogger<MyBackgroundService> logger) => _logger = logger;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            _logger.LogInformation("Background task running...");
            await Task.Delay(1000, stoppingToken);
        }
    }
}

builder.Services.AddHostedService<MyBackgroundService>();

5. Keyed DI (NET 8 이상 기능)

.NET 8에서는 Keyed DI 기능이 추가되었습니다.

builder.Services.AddKeyedSingleton<IMessageSender, EmailSender>("email");
builder.Services.AddKeyedSingleton<IMessageSender, SmsSender>("sms");

var provider = builder.Build().Services;
var emailSender = provider.GetKeyedService<IMessageSender>("email");
emailSender.SendMessage("Hello Email");

6. 정리 및 공식 문서 링크

오늘은 .NET의 DI(Dependency Injection) 개념부터 고급 패턴까지 살펴보았습니다.
DI를 적극 활용하면 코드 유지보수성, 테스트 용이성, 확장성이 크게 향상됩니다! 🚀

📌 관련 공식 문서

🔹 .NET Dependency Injection 개요
🔹 서비스 수명 주기 (Lifetime) 공식 문서
🔹 ASP.NET Core에서 DI 적용하기

+ Recent posts