MediatR를 넘어서: .NET 기본 DI로 횡단 관심사 구현

발행: (2026년 5월 22일 PM 12:45 GMT+9)
9 분 소요
원문: Dev.to

출처: Dev.to

MediatR가 불필요한 추상화일 수 있는 이유

수년간 MediatR 패키지는 거의 모든 새로운 .NET 프로젝트 템플릿에 기본으로 포함되어 왔습니다. 컨트롤러나 최소 API를 비즈니스 로직에서 분리하고 Clean Architecture 혹은 CQRS 패턴을 구현하기 위한 금본위표준으로 자주 언급되었습니다.

하지만 ASP.NET Core가 성숙해지면서 MediatR을 무거운 서드‑파티 의존성으로 추가해야 한다는 설득력 있는 아키텍처적 근거가 점점 사라지고 있습니다. 표준 CRUD 혹은 다소 복잡한 엔터프라이즈 API를 구축하고 있다면, 이제 스스로에게 물어볼 때가 왔습니다:

왜 추상화된 인‑메모리 버스를 통해 HTTP 요청을 라우팅하고, 네이티브 대안이 존재함에도 불구하고 MediatR을 사용하고 있나요?

MediatR이 코드베이스에서 불필요한 추상화가 될 수 있는 이유와, 가장 사랑받는 기능인 파이프라인 Behaviors를 순수 .NET 의존성 주입(DI)과 데코레이터 패턴만으로 대체하는 방법을 살펴보겠습니다.


MediatR이 초래하는 문제점

  • 흐려진 제어 흐름
    핸들러가 동적으로 해결되기 때문에 IDE에서 엔드포인트에서 핸들러까지 직접 “정의로 이동”(F12) 할 수 없습니다. 해당 IRequestHandler 구현을 일일이 찾아야 합니다.

  • 디버깅 오버헤드
    코드를 한 줄씩 따라가기가 힘들어 내부 라이브러리 코드‑제너레이터 스택을 우회해야 하며, 비즈니스 로직을 순차적으로 탐색하기 어렵습니다.

  • 불필요한 아키텍처
    표준 Web API에서는 엔드포인트와 핸들러 사이의 매핑이 거의 1:1입니다. 직접적인 요청‑응답 사이클에 Mediator 패턴을 도입하면 최소한의 구조적 이득에 비해 시스템이 과도하게 복잡해집니다.

만약 Minimal APIs 혹은 일반 Controllers를 사용하고 있다면 이미 강력한 엔드포인트 라우팅을 갖추고 있습니다. 그렇다면 왜 MediatR을 쓰나요? 답은 거의 항상 하나의 핵심 기능, 파이프라인 Behaviors에 있습니다.


파이프라인 Behaviors의 매력

개발자들은 MediatR이 중앙 집중식 로깅, 검증 파이프라인(예: FluentValidation), 메트릭 수집, OpenTelemetry 추적 등 횡단 관심사매우 깔끔하게 처리해 주기 때문에 사랑합니다.

모든 비즈니스 핸들러마다 반복적인 try‑catch 블록이나 명시적인 검증 호출을 넣는 대신, MediatR은 요청 주변에 일반적인 미들웨어 파이프라인을 정의할 수 있게 해 줍니다:

// 클래식 MediatR 접근 방식
builder.Services.AddMediatR(cfg =>
{
    cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
    cfg.AddOpenBehavior(typeof(LoggingBehavior));
    cfg.AddOpenBehavior(typeof(ValidationBehavior));
});

멋진 아키텍처 모델이지만, 이를 구현하기 위해 서드‑파티 패키지가 필요하지는 않습니다. 최신 .NET에서는 컴파일 타임에 타입‑안전한 데코레이터를 사용해 동일한 패턴을 네이티브하게 구현할 수 있습니다.


네이티브하게 패턴 재구성하기

1. 가벼운 요청‑핸들러 인터페이스 정의

public interface IRequestHandler
{
    Task HandleAsync(
        TRequest request,
        CancellationToken cancellationToken = default);
}

외부 패키지에 도메인을 묶지 않으면서도 CQRS 구분을 유지합니다.


2. 구체적인 비즈니스 핸들러 구현

public record CreateProductCommand(string Name, decimal Price) : IRequest;

public class CreateProductHandler : IRequestHandler
{
    private readonly IProductRepository _repository;

    public CreateProductHandler(IProductRepository repository)
    {
        _repository = repository;
    }

    public async Task HandleAsync(
        CreateProductCommand request,
        CancellationToken cancellationToken = default)
    {
        var id = Guid.NewGuid();
        // 핵심 비즈니스 로직이 여기 들어갑니다...
        return id;
    }
}

핸들러는 로깅, 텔레메트리, 검증 로직을 전혀 알지 못하므로 단일 책임 원칙을 완벽히 따릅니다.


3. 횡단 관심사 데코레이터 구현

public class LoggingHandlerDecorator : IRequestHandler
{
    private readonly IRequestHandler _inner;
    private readonly ILogger> _logger;

    public LoggingHandlerDecorator(
        IRequestHandler inner,
        ILogger> logger)
    {
        _inner = inner;
        _logger = logger;
    }

    public async Task HandleAsync(
        TRequest request,
        CancellationToken cancellationToken = default)
    {
        var requestName = typeof(TRequest).Name;
        _logger.LogInformation("Executing request: {RequestName}", requestName);

        try
        {
            var response = await _inner.HandleAsync(request, cancellationToken);
            _logger.LogInformation("Successfully executed request: {RequestName}", requestName);
            return response;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Request failed: {RequestName}", requestName);
            throw;
        }
    }
}

데코레이터는 동일한 인터페이스를 구현하고, 구체 핸들러를 의존성으로 받아 인프라 로직을 감싸줍니다.


4. 네이티브 DI를 이용해 데코레이트된 핸들러 등록

public static class RequestHandlerRegistrationExtensions
{
    public static IServiceCollection AddDecoratedRequestHandler(
        this IServiceCollection services)
        where THandler : class, IRequestHandler
    {
        // 구체 핸들러 등록
        services.AddTransient();

        // 구체 핸들러를 감싸는 데코레이터 등록
        services.AddTransient>(sp =>
        {
            var inner = sp.GetRequiredService();
            var logger = sp.GetRequiredService>>();
            return new LoggingHandlerDecorator(inner, logger);
        });

        return services;
    }
}

이 확장 메서드는 Microsoft.Extensions.DependencyInjection의 내장 서비스‑팩토리 기능을 활용해 어떤 핸들러를 어떤 순서로 데코레이트할지를 완전하게 제어합니다—외부 패키지 없이 말이죠.


정리

  • MediatR는 편리한 추상화를 제공하지만, 핵심 가치(파이프라인 Behaviors)는 네이티브 .NET DI와 데코레이터 패턴만으로도 충분히 재현할 수 있습니다.
  • 작은 명시적 핸들러 계약과 데코레이터를 정의하면 컴파일 타임 안전성을 확보하고 IDE 탐색성을 개선하며 불필요한 서드‑파티 의존성을 없앨 수 있습니다.
  • 결과적으로 MediatR이 처음 인기를 끌게 만든 횡단 관심사 기능을 그대로 유지하면서도 더 깔끔하고 유지보수하기 쉬운 코드베이스를 얻을 수 있습니다.

ASP.NET Core DI에서 데코레이트된 핸들러 등록하기

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddLoggingHandler(this IServiceCollection services)
        where THandler : class, IRequestHandler
    {
        // 1️⃣ 구체 핸들러를 자체 타입으로 등록
        services.AddTransient();

        // 2️⃣ 팩터리 메서드를 이용해 인터페이스를 등록하고 데코레이터 체인을 구성
        services.AddTransient>(sp =>
        {
            var concreteHandler = sp.GetRequiredService();
            var logger = sp.GetRequiredService>>();

            // 핵심 핸들러를 로깅 데코레이터로 감싸기
            return new LoggingHandlerDecorator(concreteHandler, logger);
        });

        return services;
    }
}

프로 팁

수천 개의 핸들러를 일일이 등록하는 것이 번거롭다면 Scrutor와 같은 어셈블리 스캔 유틸리티를 활용해 .Decorate() 메서드를 자동으로 적용할 수 있습니다.


엔드포인트에서 핸들러 사용하기

app.MapPost("/products", async (
    CreateProductCommand command,
    IRequestHandler handler) =>
{
    var result = await handler.HandleAsync(command);
    return Results.Ok(result);
});

0 조회
Back to Blog

관련 글

더 보기 »

클린 아키텍처 in .NET 설명 (The Dependency Rule)

EF Core를 업그레이드하면서 300개의 파일을 수정해야 했던 적이 있거나, 단일 비즈니스 규칙을 단위 테스트하려고 했는데 먼저 실행 중인 데이터베이스가 필요하다는 것을 깨달았다면 — 당신은...

dotnet Framework 수명 주기 도구

Introduction Learn how to create a dotnet Global Tool that lists all .NET Core frameworks with release and end‑of‑life information. 💡 For my other article on...

EF Core 명명된 쿼리 필터

Introduction EF Core 10 introduces named query filters, an enhancement over the traditional global query filters. Instead of a single combined filter per entit...