ASP.NET Core에서 Dependency Injection: 왜 존재하는가? 그리고 실제로 해결하는 문제는 무엇인가?

발행: (2026년 2월 7일 오전 12:24 GMT+9)
7 min read
원문: Dev.to

Source: Dev.to

위의 링크에 포함된 전체 텍스트를 제공해 주시면, 해당 내용을 한국어로 번역해 드리겠습니다.

소개

ASP.NET Core를 사용해 본 적이 있다면, 크게 생각하지 않고 의존성 주입(DI)을 사용했을 가능성이 높습니다. Program.cs에 서비스를 등록하고, 컨트롤러에 주입한 뒤 넘어갑니다. 하지만 DI가 도대체 왜 존재할까요? 어떤 문제를 해결하고 있는 걸까요? 코드를 통해 제대로 답해봅시다.

1. 문제: 강한 결합

아주 흔한 초보자 패턴은 다음과 같습니다:

public class PostsController : ControllerBase
{
    private readonly PostsService _service = new PostsService();

    [HttpGet]
    public IEnumerable Get()
    {
        return _service.GetPosts();
    }
}

public class PostsService
{
    public IEnumerable GetPosts()
    {
        return new[] { "Post A", "Post B" };
    }
}

언뜻 보기에는 괜찮아 보입니다. 코드가 동작하고 API도 실행됩니다.

왜 이것이 문제가 되는가

❌ 변경 비용이 높다

내일 PostsService가 데이터베이스 접근이 필요하거나, 새로운 구현이 필요하거나, 테스트를 위한 모크가 필요하게 되면, 컨트롤러 코드를 수정해야 합니다. 이는 다음을 의미합니다:

  • 여러 파일을 수정해야 함
  • 버그 발생 위험
  • 확장에는 열려 있고, 수정에는 닫혀 있다 원칙 위반

❌ 테스트가 어려움

컨트롤러가 직접 의존성을 생성하기 때문에 쉽게 교체할 수 없습니다. 다음과 같이 강제됩니다:

  • 실제 데이터베이스에 접근
  • 실제 API 호출
  • 또는 로직을 우회하는 해킹 코드를 작성

이것이 전형적인 강한 결합입니다.

2. 의존성 주입의 핵심 아이디어 (한 줄)

클래스 내부에서 의존성을 생성하지 마세요. 의존성을 요청하세요.

“어떤 서비스를 사용할지 내가 결정하겠다”라고 말하는 대신, “이 계약을 따르는 무언가를 주세요”라고 말합니다.

3. 해결책: 추상화 + 생성자 주입

계약 정의 (인터페이스)

public interface IPostsService
{
    IEnumerable GetPosts();
}

계약 구현

public class PostsService : IPostsService
{
    public IEnumerable GetPosts()
    {
        return new[] { "Post A", "Post B" };
    }
}

컨트롤러에 주입하기

[ApiController]
[Route("api/posts")]
public class PostsController : ControllerBase
{
    private readonly IPostsService _service;

    public PostsController(IPostsService service)
    {
        _service = service;
    }

    [HttpGet]
    public IEnumerable Get()
    {
        return _service.GetPosts();
    }
}

무엇이 바뀌었나요?

  • 컨트롤러는 이제 어떤 구현을 사용하는지 알지 못합니다; 추상화에만 의존합니다.
  • 여기서부터 결합도가 낮아지기 시작합니다.

4. ASP.NET Core가 의존성을 해결하도록 하세요

DI 컨테이너에 서비스를 등록합니다:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

// One‑line decision
builder.Services.AddScoped();

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

나중에 다른 구현을 추가하면:

public class PostsServiceNew : IPostsService
{
    public IEnumerable GetPosts()
    {
        return new[] { "Post X", "Post Y" };
    }
}

다음과 같이 교체할 수 있습니다:

builder.Services.AddScoped();

컨트롤러를 변경할 필요 없으며, 파급 효과도 없습니다.

5. DI 컨테이너가 실제로 하는 일

배경에서 ASP.NET Core는:

  1. 컨트롤러를 생성합니다.
  2. IPostsService가 필요함을 감지합니다.
  3. 컨테이너에서 등록 정보를 조회합니다.
  4. 올바른 구현을 인스턴스화합니다.
  5. 자동으로 주입합니다.

당신은 제어권을 잃은 것이 아니라 책임을 프레임워크에 넘긴 것입니다. 이것이 제어 역전(Inversion of Control, IoC) 입니다.

6. SOLID와의 연관성 (간략히)

  • Abstraction – 구현이 아니라 인터페이스에 맞춰 프로그래밍합니다.
  • Decoupling – 컨트롤러는 어떤 서비스가 사용되는지 알 필요도 없고 신경 쓰지도 않습니다.
  • Dependency Inversion Principle (DIP) – 고수준 모듈은 구체 클래스가 아니라 추상에 의존합니다.
  • Inversion of Control (IoC) – 프레임워크가 객체를 생성하고 연결해 줍니다.

DI는 이러한 원칙들을 실제로 적용한 것입니다.

7. 왜 DI가 테스트를 쉽게 만드는가

Because dependencies are injected, you can pass a fake implementation:

public class FakePostsService : IPostsService
{
    public IEnumerable GetPosts()
    {
        return new[] { "Fake Post" };
    }
}
var controller = new PostsController(new FakePostsService());
var result = controller.Get();

No database, no infrastructure—just pure logic. This is where DI really pays off.

8. 의존성 주입이 유용한 경우 ✅

  • 교체 가능한 구현이 필요할 때.
  • 테스트 가능성을 중요하게 생각할 때.
  • 중대형 애플리케이션을 구축할 때.
  • 요구사항이 변할 것으로 예상될 때.

9. When DI is overkill ❌

  • 매우 작은 스크립트.
  • 일회성 콘솔 애플리케이션.
  • 절대 성장하거나 테스트되지 않을 코드.

DI는 도구일 뿐—규칙이 아니다.

Back to Blog

관련 글

더 보기 »

CodeBehind 4.5 출시; 고급 비동기 기능

개요 Version 4.5는 .NET 7을 기반으로 구축된 CodeBehind 프레임워크의 최신 릴리스입니다. 곧 출시될 4.6 버전은 .NET 10을 목표로 하며 현재 개발 중입니다.