본문 바로가기
Language/C#

C# DI(Dependency Injection)와 DI 컨테이너 정리

by Wikinist 2025. 4. 27.

클래스 간의 의존성을 효과적으로 관리하기 위해, C#에서는 의존성 주입(Dependency Injection) 패턴과 DI 컨테이너를 널리 사용합니다.
이 글에서는 DI의 개념부터 DI 컨테이너의 사용법까지 체계적으로 정리해봅니다.


1. DI(Dependency Injection)란 무엇인가?

1.1 DI를 쉽게 설명하면

"내가 직접 필요한 객체를 만들지 않고, 외부에서 만들어서 주입해주는 것"을 의미합니다.

1.2 일반 방식 (직접 생성)

public class MyController
{
    private readonly Logger _logger;

    public MyController()
    {
        _logger = new Logger(); // 직접 생성
    }

    public void DoSomething()
    {
        _logger.Log("일을 시작합니다.");
    }
}

문제점

  • Logger 클래스를 직접 생성하기 때문에, 나중에 다른 로거(FileLogger, ConsoleLogger 등)로 바꿀 때 코드 수정이 필요합니다.
  • 테스트할 때도 쉽게 Mock 객체를 주입할 수 없습니다.

1.3 DI 방식 (외부 주입)

public class MyController
{
    private readonly ILogger _logger;

    public MyController(ILogger logger) // 외부에서 주입
    {
        _logger = logger;
    }

    public void DoSomething()
    {
        _logger.Log("일을 시작합니다.");
    }
}

장점

  • 어떤 종류의 로거가 들어올지 알 필요 없이, 인터페이스만 알면 됩니다.
  • 테스트 시 가짜(Mock) 로거를 쉽게 주입할 수 있습니다.
  • 구조가 유연해지고 변경에 강해집니다.

1.4 핵심 정리

  • 의존성(Dependency): 내가 필요로 하는 객체들 (Logger, Database, API Client 등)
  • 주입(Injection): 직접 생성하지 않고 외부로부터 객체를 전달받는 것

의존성 주입을 활용하면, 유지보수성, 테스트 편의성, 확장성이 모두 크게 향상됩니다.


2. DI 컨테이너란 무엇인가?

2.1 DI 컨테이너 개념 요약

"필요한 의존성 객체를 대신 생성하고, 자동으로 클래스에 주입해주는 도구"를 말합니다.

2.2 구체적인 역할

  • 인터페이스와 구현체를 등록해 연결해둡니다.
  • 생성자에 필요한 객체를 자동으로 생성해 넣어줍니다.

2.3 DI 컨테이너를 활용한 예시

서비스 등록 단계

var services = new ServiceCollection();
services.AddSingleton<ILogger, ConsoleLogger>();
services.AddTransient<IMyService, MyService>();

컨트롤러 생성 시

public class MyController
{
    private readonly ILogger _logger;

    public MyController(ILogger logger)
    {
        _logger = logger;
    }
}

DI 컨테이너가 MyController를 생성할 때, ILogger 자리에 자동으로 ConsoleLogger를 넣어줍니다.


3. DI 컨테이너를 사용한 객체 생성 흐름

3.1 DI 없이 직접 생성하는 경우

var logger = new ConsoleLogger();
var controller = new MyController(logger);
controller.Run();
  • 객체를 직접 생성해서 넣어야 합니다.
  • 결합도가 높아지기 쉽습니다.

3.2 DI 컨테이너로 생성하는 경우

using Microsoft.Extensions.DependencyInjection;

var services = new ServiceCollection();
services.AddSingleton<ILogger, ConsoleLogger>();
services.AddTransient<MyController>();

var provider = services.BuildServiceProvider();
var controller = provider.GetRequiredService<MyController>();
controller.Run();
  • 등록만 하면, 필요한 의존성들이 자동으로 주입됩니다.
  • 느슨한 결합(loose coupling) 구조를 자연스럽게 가질 수 있습니다.

4. 객체 생명주기(Lifetime) 관리

DI 컨테이너는 객체를 생성할 때 생명주기도 함께 관리할 수 있습니다.

생명주기 종류 설명 사용 예시
Singleton 애플리케이션 시작부터 끝까지 하나의 인스턴스만 사용 설정 객체, 캐시, 로깅 서비스
Scoped 웹 요청 단위로 하나의 인스턴스를 사용 (ASP.NET Core 한정) DBContext, 사용자 인증
Transient 요청할 때마다 새 인스턴스를 생성 가벼운 유틸성 클래스, Stateless 서비스

4.1 Singleton 등록 예시

services.AddSingleton<IMyService, MyService>();

항상 동일한 인스턴스를 반환합니다.

4.2 Transient 등록 예시

services.AddTransient<IMyService, MyService>();

매번 새로운 인스턴스를 생성합니다.


5. services 객체의 역할

5.1 services란?

services는 IServiceCollection 타입의 객체로,
"어떤 인터페이스에 어떤 구현체를 연결할지"를 등록하는 컨테이너입니다.

5.2 services를 사용하는 이유

  • 프로젝트에 필요한 객체들을 DI를 통해 주입할 수 있도록 구성합니다.
  • 객체의 생명주기(Lifetime)도 함께 설정할 수 있습니다.
  • 이후 BuildServiceProvider()를 통해 서비스 프로바이더를 생성하고, 주입받을 수 있습니다.
var services = new ServiceCollection();
services.AddSingleton<ILogger, ConsoleLogger>();
services.AddTransient<MyController>();

var provider = services.BuildServiceProvider();
var controller = provider.GetRequiredService<MyController>();

마무리

DI(Dependency Injection)와 DI 컨테이너를 제대로 이해하고 활용하면,
유지보수, 테스트 편의성, 구조적 유연성을 모두 갖춘 안정적인 코드를 작성할 수 있습니다.

C#을 사용하는 프로젝트에서는 반드시 익혀야 할 필수 개념입니다.

해당 게시글은 ChatGPT의 도움을 받아 작성되었습니다.