DI(의존성 주입) 컨테이너

By | 2026년 10월 28일

DI(의존성 주입) 컨테이너 상세 설명

1. 먼저 “의존성”이란?

어떤 클래스가 동작하려고 다른 클래스를 필요로 하면, 그 클래스가 의존성입니다.

csharp

public class ApprovalService
{
    private readonly CountryRepository _repo;

    public ApprovalService()
    {
        _repo = new CountryRepository();   // 직접 생성
    }
}

ApprovalService는 CountryRepository에 의존합니다. 위처럼 안에서 직접 new 하면 문제가 생깁니다.

문제설명
강한 결합CountryRepository를 바꾸려면 ApprovalService도 수정해야 함
테스트 어려움가짜(Mock) 객체로 바꿔 끼울 수 없음
생성 복잡도CountryRepository가 또 다른 의존성(DB 연결 등)을 요구하면 new 체인이 길어짐
수명 관리 불가공유할지, 매번 만들지 결정할 곳이 없음

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

필요한 객체를 내가 만들지 않고, 밖에서 넣어 주는 방식입니다.

csharp

public class ApprovalService
{
    private readonly CountryRepository _repo;

    public ApprovalService(CountryRepository repo)   // 생성자로 "받음"
    {
        _repo = repo;
    }
}

ApprovalService는 “누가 만들었는지” 모르고, 그냥 받아서 씁니다. 이를 제어의 역전(IoC, Inversion of Control) 이라고도 합니다. 객체 생성의 제어권이 클래스에서 외부로 넘어갔기 때문입니다.

3. DI 컨테이너란?

위처럼 “밖에서 넣어 주는” 일을 자동으로 해 주는 장치입니다. ASP.NET Core에는 기본 내장되어 있고, builder.Services가 바로 그 컨테이너입니다.

컨테이너가 하는 일은 세 가지입니다.

  1. 등록: “이 타입은 이렇게 만들어라” (우리가 AddScoped 등으로 알려 줌)
  2. 생성: 필요할 때 생성자를 분석해 의존성을 재귀적으로 만들어 냄
  3. 수명 관리: 언제 만들고 언제 버릴지 관리 (Dispose까지 자동 호출)

4. 이 프로젝트에서의 동작 흐름

등록:

csharp

builder.Services.AddScoped<CountryRepository>();
builder.Services.AddScoped<ApprovalService>();
builder.Services.AddSingleton<IsoCatalog>();

컨트롤러(가정):

csharp

public class CountryController : ControllerBase
{
    private readonly ApprovalService _approval;

    public CountryController(ApprovalService approval)   // 컨트롤러는 이것만 요청
    {
        _approval = approval;
    }
}

요청이 들어오면 컨테이너가 이렇게 처리합니다.

요청 도착
  └─ CountryController 필요
       └─ 생성자에 ApprovalService 필요
            └─ 생성자에 CountryRepository 필요 → 생성
            └─ (필요하면) IsoCatalog → 이미 있는 싱글톤 재사용
            ← ApprovalService 생성 완료
       ← CountryController 생성 완료
  └─ 요청 처리 후, 요청이 끝나면 Scoped 객체들 Dispose

우리는 new를 한 번도 쓰지 않았는데 연결이 끝까지 이어집니다. 의존성이 몇 단계로 깊어져도 컨테이너가 알아서 풀어 줍니다.

5. 수명(Lifetime) 다시 정리

수명생성 시점소멸 시점예
Singleton처음 요청될 때 1번앱 종료IsoCatalog
ScopedHTTP 요청마다 1번요청 종료CountryRepository
Transient주입받을 때마다스코프 종료 시 Dispose가벼운 유틸

같은 요청 안에서 ApprovalService와 CountryController가 둘 다 CountryRepository를 받으면 같은 인스턴스를 공유합니다 (Scoped이므로). 이것이 같은 DB 트랜잭션/컨텍스트를 요청 안에서 공유하게 해 주는 원리입니다.

6. 인터페이스와 함께 쓰는 것이 일반적

이 프로젝트는 구체 클래스를 직접 등록했지만, 보통은 인터페이스로 등록합니다.

csharp

builder.Services.AddScoped<ICountryRepository, CountryRepository>();

csharp

public ApprovalService(ICountryRepository repo) { ... }

이러면 구현을 바꿔도(예: SQL Server → PostgreSQL) 등록 한 줄만 바꾸면 되고, 테스트에서는 가짜 구현을 끼울 수 있습니다.

csharp

// 테스트 예
var fake = new FakeCountryRepository();
var service = new ApprovalService(fake);   // 컨테이너 없이도 주입 가능

7. 주입 방법

방법예
생성자 주입 (가장 일반적)public Foo(Bar bar)
메서드 주입컨트롤러 액션에 [FromServices] Bar bar
미들웨어 Invoke 파라미터Invoke(HttpContext ctx, Bar bar)
Minimal API 파라미터app.MapGet("/", (Bar bar) => ...)

8. 자주 하는 실수

1) 등록 안 하고 주입 요청

InvalidOperationException: Unable to resolve service for type 'CountryRepository'

AddXxx를 빠뜨리면 나는 가장 흔한 오류입니다.

2) 수명 불일치 (Captive Dependency)
Singleton이 Scoped를 주입받으면, Scoped 객체가 요청이 끝나도 살아남아 오래된 DB 연결을 쥐게 됩니다. 개발 환경에서는 예외로 알려 줍니다. 규칙은 “긴 수명이 짧은 수명에 의존하지 않기”입니다.

3) 순환 의존
A가 B를, B가 A를 생성자에서 요구하면 컨테이너가 만들 수 없습니다. 설계를 나눠야 합니다.

4) 서비스 로케이터 남용
IServiceProvider에서 직접 GetService<T>()로 꺼내 쓰면 의존성이 숨겨져 DI의 장점이 사라집니다. 생성자 주입을 우선하세요.

9. 왜 쓰나, 한 줄 요약

장점설명
느슨한 결합구현 교체가 쉬움
테스트 용이Mock/Fake 주입 가능
수명 일괄 관리생성·공유·해제를 컨테이너가 담당
코드 단순화new 체인 제거, 설정이 한곳(Program.cs)에 모임

정리하면 “클래스는 필요한 것을 선언만 하고, 만들고 연결하는 일은 builder.Services가 한다” 가 DI 컨테이너의 핵심입니다.