C# Minimal API: 출력 캐싱

발행: (2025년 12월 20일 오전 02:56 GMT+9)
5 min read
원문: Dev.to

Source: Dev.to

서버에 생성된 응답을 저장하고 엔드포인트를 다시 실행하지 않고 직접 제공한다.
Microsoft Docs

Output caching은 미들웨어로 구현된 서버‑사이드 캐싱 메커니즘이다. 전체 HTTP 응답을 저장하고 이후 요청에 대해 엔드포인트를 다시 실행하지 않고 제공한다.

  • 기본적으로 인‑메모리 저장소를 사용하지만 Redis와 같은 분산 저장소를 백엔드로 사용할 수 있다.
  • 정의된 시간 창 내에 동일한 응답을 반환하는 비용이 많이 드는 서버‑사이드 작업에 가장 적합하다.

How it works

When a request arrives:

  1. Cache hit – a cached response exists → the middleware short‑circuits the pipeline and returns the cached response.
  2. Cache miss – no cached response → the request is processed normally, and the response is cached for future requests.

출력 캐시 추가

  1. AddOutputCache()로 서비스를 등록합니다.
  2. 하나 이상의 정책을 정의합니다.
  3. 정책을 엔드포인트에 적용합니다.
  4. UseOutputCache()로 미들웨어를 추가합니다.
var builder = WebApplication.CreateBuilder(args);

// 1️⃣ Register output‑cache services
builder.Services.AddOutputCache(options =>
{
    // Default policy (10 s)
    options.AddBasePolicy(policy => policy.Expire(TimeSpan.FromSeconds(10)));

    // Named policy (20 s)
    options.AddPolicy("OutputCache20Seconds", policy => policy.Expire(TimeSpan.FromSeconds(20)));
});

var app = builder.Build();

// 2️⃣ Apply policies to Minimal API endpoints
app.MapGet("/default-cache-policy", () => new[] { "someresponse2" })
   .CacheOutput();                         // uses default policy

app.MapGet("/custom-cache-policy", () => new[] { "someresponse" })
   .CacheOutput("OutputCache20Seconds");   // uses named policy

// 3️⃣ Add the middleware (must be after UseCors() if you use CORS)
app.UseOutputCache();

app.Run();

Note: CORS 미들웨어를 사용하는 앱에서는 UseOutputCache()UseCors() 이후에 호출해야 합니다.

Authorization 헤더가 있는 경우 출력 캐싱

기본적으로 Authorization 헤더가 존재하면 출력 캐싱은 응답을 캐시하지 않습니다 – 이는 인증된 콘텐츠가 잘못된 사용자에게 제공되는 것을 방지하기 위한 안전 조치입니다.

이 동작을 재정의하려면 사용자 정의 출력‑캐시 정책을 생성하십시오:

internal static class CustomOutputCachingPolicyFactory
{
    internal static CustomOutputCachingPolicy Create(TimeSpan expiration)
        => new(expiration);
}

internal sealed class CustomOutputCachingPolicy : IOutputCachePolicy
{
    private readonly TimeSpan _expiration;

    internal CustomOutputCachingPolicy(TimeSpan expiration) => _expiration = expiration;

    public ValueTask CacheRequestAsync(OutputCacheContext context, CancellationToken cancellation)
    {
        var canCache = HttpMethods.IsGet(context.HttpContext.Request.Method) ||
                       HttpMethods.IsHead(context.HttpContext.Request.Method);

        context.EnableOutputCaching = canCache;
        context.AllowCacheLookup   = canCache;
        context.AllowCacheStorage  = canCache;
        context.AllowLocking       = true;
        context.ResponseExpirationTimeSpan = _expiration;
        context.CacheVaryByRules.QueryKeys = "*";

        return ValueTask.CompletedTask;
    }

    public ValueTask ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellation)
        => ValueTask.CompletedTask;

    public ValueTask ServeResponseAsync(OutputCacheContext context, CancellationToken cancellation)
    {
        var response = context.HttpContext.Response;

        // Do not cache if Set‑Cookie header is present
        if (!StringValues.IsNullOrEmpty(response.Headers.SetCookie))
        {
            context.AllowCacheStorage = false;
            return ValueTask.CompletedTask;
        }

        // Only cache successful (200) or permanent redirect (301) responses
        if (response.StatusCode is not (StatusCodes.Status200OK or StatusCodes.Status301MovedPermanently))
        {
            context.AllowCacheStorage = false;
        }

        return ValueTask.CompletedTask;
    }
}

Minimal API용 편리한 확장

이 확장 기능은 사용자 정의 정책 등록을 간소화합니다:

public static class OutputCachingExtensions
{
    public static void AddCustomOutputCachingPolicy(
        this OutputCacheOptions options,
        params (string Name, TimeSpan Expiration)[] policies)
    {
        foreach (var (name, expiration) in policies)
        {
            options.AddPolicy(name,
                CustomOutputCachingPolicyFactory.Create(expiration));
        }
    }

    public static IServiceCollection AddOutputCacheWithCustomPolicy(
        this IServiceCollection services,
        params (string Name, TimeSpan Expiration)[] policies) =>
        services.AddOutputCache(opts => opts.AddCustomOutputCachingPolicy(policies));

    public static IServiceCollection AddOutputCacheWithCustomPolicy(
        this IServiceCollection services,
        Action<OutputCacheOptions> configure,
        params (string Name, TimeSpan Expiration)[] policies) =>
        services.AddOutputCache(opts =>
        {
            configure(opts);
            opts.AddCustomOutputCachingPolicy(policies);
        });
}

추가 헬퍼 메서드:

options.AddCustomOutputCachingPolicy(policies);
configureOptions.Invoke(options);
});

public static RouteHandlerBuilder CustomCacheOutput(this RouteHandlerBuilder routeHandlerBuilder, string name)
    => routeHandlerBuilder.CacheOutput(name);

중요: 응답이 모든 사용자에게 동일한 경우가 아니면 인증된 엔드포인트나 사용자별 엔드포인트에 출력 캐싱을 사용하지 마세요.

언제 사용해야 할까

응답을 생성 비용이 많이 들고 자주 변경되지 않을 때 출력 캐싱을 사용합니다:

  • 비용이 많이 드는 서버‑사이드 연산
  • 실행 비용이 높은 빈번히 요청되는 응답 (예: 보고서 생성, 사용자 프로필 렌더링)
Back to Blog

관련 글

더 보기 »