다음을 통해 공유


ASP.NET Core의 출력 캐싱 미들웨어

작성자: Tom Dykstra

참고 항목

이 문서의 최신 버전은 아닙니다. 현재 릴리스는 이 문서의 .NET 9 버전을 참조 하세요.

Warning

이 버전의 ASP.NET Core는 더 이상 지원되지 않습니다. 자세한 내용은 .NET 및 .NET Core 지원 정책을 참조 하세요. 현재 릴리스는 이 문서의 .NET 9 버전을 참조 하세요.

Important

이 정보는 상업적으로 출시되기 전에 실질적으로 수정될 수 있는 시험판 제품과 관련이 있습니다. Microsoft는 여기에 제공된 정보에 대해 어떠한 명시적, 또는 묵시적인 보증을 하지 않습니다.

현재 릴리스는 이 문서의 .NET 9 버전을 참조 하세요.

이 문서에서는 ASP.NET Core 앱에서 출력 캐싱 미들웨어를 구성하는 방법을 설명합니다. 출력 캐싱에 대한 소개는 출력 캐싱을 참조하세요.

출력 캐싱 미들웨어는 최소 API, 컨트롤러가 있는 Web API, MVC 및 Razor Pages와 같은 모든 유형의 ASP.NET Core 앱에서 사용할 수 있습니다. 최소 API 및 컨트롤러 기반 API에 대한 코드 예제가 제공됩니다. 컨트롤러 기반 API 예제에서는 특성을 사용하여 캐싱을 구성하는 방법을 보여 줍니다. 이러한 특성은 MVC 및 Razor Pages 앱에서도 사용할 수 있습니다.

코드 예제는 이미지를 생성하고 "생성된 날짜 및 시간"을 제공하는 Gravatar 클래스를 참조합니다. 클래스는 샘플 앱에서만 정의되고 사용됩니다. 그 목적은 캐시된 출력이 사용되는 시기를 쉽게 확인할 수 있도록 하는 것입니다. 자세한 내용은 샘플 코드에서 샘플전처리기 지시문을 다운로드하는 방법을 참조하세요.

앱에 미들웨어 추가

AddOutputCache를 호출하여 서비스 컬렉션에 출력 캐싱 미들웨어를 추가합니다.

UseOutputCache를 호출하여 요청 처리 파이프라인에 미들웨어를 추가합니다.

예시:

builder.Services.AddOutputCache();
var app = builder.Build();

// Configure the HTTP request pipeline.
app.UseHttpsRedirection();
app.UseOutputCache();
app.UseAuthorization();

AddOutputCacheUseOutputCache를 호출하면 캐싱 동작이 시작되지 않으므로 캐싱을 사용할 수 있습니다. 앱 캐시 응답을 만들려면 다음 섹션과 같이 캐싱을 구성해야 합니다.

참고 항목

  • CORS 미들웨어를 사용하는 앱에서는 UseCors 이후에 UseOutputCache를 호출해야 합니다.
  • Razor Pages 앱 및 컨트롤러가 있는 앱에서 UseRouting 이후에 UseOutputCache를 호출해야 합니다.

하나의 엔드포인트 또는 페이지 구성

최소 API 앱의 경우 다음 예제와 같이 CacheOutput을 호출하거나 [OutputCache] 특성을 적용하여 캐싱을 수행하도록 엔드포인트를 구성합니다.

app.MapGet("/cached", Gravatar.WriteGravatar).CacheOutput();
app.MapGet("/attribute", [OutputCache] (context) => 
    Gravatar.WriteGravatar(context));

컨트롤러가 있는 앱의 [OutputCache] 경우 다음과 같이 작업 메서드에 특성을 적용합니다.

[ApiController]
[Route("/[controller]")]
[OutputCache]
public class CachedController : ControllerBase
{
    public async Task GetAsync()
    {
        await Gravatar.WriteGravatar(HttpContext);
    }
}

Razor Pages 앱의 경우 Razor 페이지 클래스에 특성을 적용합니다.

여러 엔드포인트 또는 페이지 구성

여러 엔드포인트에 적용되는 캐싱 구성을 지정하기 위해 호출 AddOutputCache 할 때 정책을 만듭니다. 특정 엔드포인트에 대해 정책을 선택할 수 있지만 기본 정책은 엔드포인트 컬렉션에 대한 기본 캐싱 구성을 제공합니다.

다음 강조 표시된 코드는 만료 시간이 10초인 앱의 모든 엔드포인트에 대한 캐싱을 구성합니다. 만료 시간을 지정하지 않으면 기본값은 1분입니다.

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => 
        builder.Expire(TimeSpan.FromSeconds(10)));
    options.AddPolicy("Expire20", builder => 
        builder.Expire(TimeSpan.FromSeconds(20)));
    options.AddPolicy("Expire30", builder => 
        builder.Expire(TimeSpan.FromSeconds(30)));
});

다음 강조 표시된 코드는 각각 다른 만료 시간을 지정하는 두 개의 정책을 만듭니다. 선택한 엔드포인트는 20초 만료를 사용할 수 있으며, 다른 엔드포인트는 30초 만료를 사용할 수 있습니다.

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => 
        builder.Expire(TimeSpan.FromSeconds(10)));
    options.AddPolicy("Expire20", builder => 
        builder.Expire(TimeSpan.FromSeconds(20)));
    options.AddPolicy("Expire30", builder => 
        builder.Expire(TimeSpan.FromSeconds(30)));
});

메서드를 호출하거나 특성을 사용할 [OutputCache] 때 엔드포인트에 CacheOutput 대한 정책을 선택할 수 있습니다.

최소 API 앱에서 다음 코드는 20초 만료와 30초 만료가 있는 엔드포인트 하나를 구성합니다.

app.MapGet("/20", Gravatar.WriteGravatar).CacheOutput("Expire20");
app.MapGet("/30", [OutputCache(PolicyName = "Expire30")] (context) => 
    Gravatar.WriteGravatar(context));

컨트롤러가 있는 앱의 [OutputCache] 경우 작업 메서드에 특성을 적용하여 정책을 선택합니다.

[ApiController]
[Route("/[controller]")]
[OutputCache(PolicyName = "Expire20")]
public class Expire20Controller : ControllerBase
{
    public async Task GetAsync()
    {
        await Gravatar.WriteGravatar(HttpContext);
    }
}

Razor Pages 앱의 경우 Razor 페이지 클래스에 특성을 적용합니다.

기본 출력 캐싱 정책

기본적으로 출력 캐싱은 다음 규칙을 따릅니다.

  • HTTP 200 응답만 캐시됩니다.
  • HTTP GET 또는 HEAD 요청만 캐시됩니다.
  • 쿠키를 설정하는 응답은 캐시되지 않습니다.
  • 인증된 요청에 대한 응답은 캐시되지 않습니다.

다음 코드는 모든 기본 캐싱 규칙을 앱의 모든 엔드포인트에 적용합니다.

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder.Cache());
});

기본 정책 재정의

다음 코드에서는 기본값 규칙을 재정의하는 방법을 보여 줍니다. 다음 사용자 지정 정책 코드에서 강조 표시된 줄은 HTTP POST 메서드 및 HTTP 301 응답에 대한 캐싱을 사용하도록 설정합니다.

using Microsoft.AspNetCore.OutputCaching;
using Microsoft.Extensions.Primitives;

namespace OCMinimal;

public sealed class MyCustomPolicy : IOutputCachePolicy
{
    public static readonly MyCustomPolicy Instance = new();

    private MyCustomPolicy()
    {
    }

    ValueTask IOutputCachePolicy.CacheRequestAsync(
        OutputCacheContext context, 
        CancellationToken cancellationToken)
    {
        var attemptOutputCaching = AttemptOutputCaching(context);
        context.EnableOutputCaching = true;
        context.AllowCacheLookup = attemptOutputCaching;
        context.AllowCacheStorage = attemptOutputCaching;
        context.AllowLocking = true;

        // Vary by any query by default
        context.CacheVaryByRules.QueryKeys = "*";

        return ValueTask.CompletedTask;
    }

    ValueTask IOutputCachePolicy.ServeFromCacheAsync
        (OutputCacheContext context, CancellationToken cancellationToken)
    {
        return ValueTask.CompletedTask;
    }

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

        // Verify existence of cookie headers
        if (!StringValues.IsNullOrEmpty(response.Headers.SetCookie))
        {
            context.AllowCacheStorage = false;
            return ValueTask.CompletedTask;
        }

        // Check response code
        if (response.StatusCode != StatusCodes.Status200OK && 
            response.StatusCode != StatusCodes.Status301MovedPermanently)
        {
            context.AllowCacheStorage = false;
            return ValueTask.CompletedTask;
        }

        return ValueTask.CompletedTask;
    }

    private static bool AttemptOutputCaching(OutputCacheContext context)
    {
        // Check if the current request fulfills the requirements
        // to be cached
        var request = context.HttpContext.Request;

        // Verify the method
        if (!HttpMethods.IsGet(request.Method) && 
            !HttpMethods.IsHead(request.Method) && 
            !HttpMethods.IsPost(request.Method))
        {
            return false;
        }

        // Verify existence of authorization headers
        if (!StringValues.IsNullOrEmpty(request.Headers.Authorization) || 
            request.HttpContext.User?.Identity?.IsAuthenticated == true)
        {
            return false;
        }

        return true;
    }
}

이 사용자 지정 정책을 사용하려면 명명된 정책을 만듭니다.

builder.Services.AddOutputCache(options =>
{
    options.AddPolicy("CachePost", MyCustomPolicy.Instance);
});

엔드포인트에 대해 명명된 정책을 선택합니다. 다음 코드는 최소 API 앱에서 엔드포인트에 대한 사용자 지정 정책을 선택합니다.

app.MapPost("/cachedpost", Gravatar.WriteGravatar)
    .CacheOutput("CachePost");

다음 코드는 컨트롤러 작업에 대해 동일한 작업을 수행합니다.

[ApiController]
[Route("/[controller]")]
[OutputCache(PolicyName = "CachePost")]
public class PostController : ControllerBase
{
    public async Task GetAsync()
    {
        await Gravatar.WriteGravatar(HttpContext);
    }
}

대체 기본 정책 재정의

또는 DI(종속성 주입)를 사용하여 사용자 지정 정책 클래스를 다음과 같이 변경하여 인스턴스를 초기화합니다.

  • 프라이빗 생성자 대신 퍼블릭 생성자입니다.
  • 사용자 지정 정책 클래스에서 Instance 속성을 제거합니다.

예시:

public sealed class MyCustomPolicy2 : IOutputCachePolicy
{

    public MyCustomPolicy2()
    {
    }

클래스의 나머지는 이전에 표시된 것과 동일합니다. 다음 예제와 같이 사용자 지정 정책을 추가합니다.

builder.Services.AddOutputCache(options =>
{
    options.AddPolicy("CachePost", builder => 
        builder.AddPolicy<MyCustomPolicy2>(), true);
});

앞의 코드는 DI를 사용하여 사용자 지정 정책 클래스의 인스턴스를 만듭니다. 생성자의 모든 퍼블릭 인수가 확인됩니다.

사용자 지정 정책을 기본 정책으로 사용하는 경우 인수 없이 호출 OutputCache() 하거나 기본 정책이 적용해야 하는 엔드포인트에서 특성을 사용하지 [OutputCache] 마세요. 특성을 호출 OutputCache() 하거나 사용하면 엔드포인트에 기본 정책이 추가됩니다.

캐시 경로 지정

기본적으로 URL의 모든 부분은 캐시 항목, 즉 스키마, 호스트, 포트, 경로 및 쿼리 문자열의 키로 포함됩니다. 그러나 캐시 키를 명시적으로 제어할 수 있습니다. 예를 들어 culture 쿼리 문자열의 각 고유 값에 대해서만 고유한 응답을 반환하는 엔드포인트가 있다고 가정합니다. 다른 쿼리 문자열과 같은 URL의 다른 부분의 변형으로 인해 캐시 항목이 달라져서는 안 됩니다. 다음 강조 표시된 코드와 같이 정책에서 이러한 규칙을 지정할 수 있습니다.

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder
        .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
        .Tag("tag-blog"));
    options.AddBasePolicy(builder => builder.Tag("tag-all"));
    options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
    options.AddPolicy("NoCache", builder => builder.NoCache());
    options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});

그런 다음 엔드포인트에 VaryByQuery 대한 정책을 선택할 수 있습니다. 최소 API 앱에서 다음 코드는 쿼리 문자열의 VaryByQuery 각 고유 값에 대해서만 고유한 응답을 반환하는 엔드포인트에 culture 대한 정책을 선택합니다.

app.MapGet("/query", Gravatar.WriteGravatar).CacheOutput("Query");

다음 코드는 컨트롤러 작업에 대해 동일한 작업을 수행합니다.

[ApiController]
[Route("/[controller]")]
[OutputCache(PolicyName = "Query")]
public class QueryController : ControllerBase
{
    public async Task GetAsync()
    {
        await Gravatar.WriteGravatar(HttpContext);
    }
}

다음은 캐시 키를 제어하기 위한 몇 가지 옵션입니다.

  • SetVaryByQuery - 캐시 키에 추가할 하나 이상의 쿼리 문자열 이름을 지정합니다.

  • SetVaryByHeader - 캐시 키에 추가할 HTTP 헤더를 하나 이상 지정합니다.

  • VaryByValue- 캐시 키에 추가할 값을 지정합니다. 다음 예제에서는 현재 서버 시간(초)이 홀수인지 짝수인지를 나타내는 값을 사용합니다. 새 응답은 초 수가 홀수에서 짝수 또는 홀수로 가는 경우에만 생성됩니다.

    builder.Services.AddOutputCache(options =>
    {
        options.AddBasePolicy(builder => builder
            .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
            .Tag("tag-blog"));
        options.AddBasePolicy(builder => builder.Tag("tag-all"));
        options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
        options.AddPolicy("NoCache", builder => builder.NoCache());
        options.AddPolicy("NoLock", builder => builder.SetLocking(false));
        options.AddPolicy("VaryByValue", builder => 
            builder.VaryByValue((context) =>
                new KeyValuePair<string, string>(
                "time", (DateTime.Now.Second % 2)
                    .ToString(CultureInfo.InvariantCulture))));
    });
    

OutputCacheOptions.UseCaseSensitivePaths를 사용하여 키의 경로 부분이 대/소문자를 구분하도록 지정합니다. 기본값은 대/소문자 구분입니다.

자세한 옵션은 OutputCachePolicyBuilder 클래스를 참조하세요.

캐시 유효성 재검사

캐시 유효성 재검사로서 서버가 전체 응답 본문 대신 304 Not Modified HTTP 상태 코드를 반환할 수 있게 됩니다. 이 상태 코드는 요청에 대한 응답이 클라이언트가 이전에 받은 응답과 변경되지 않음을 클라이언트에 알릴 수 있습니다.

다음 코드에서는 Etag 헤더를 사용하여 캐시 유효성 검사를 사용하도록 설정하는 방법을 보여 줍니다. 클라이언트가 이전 응답의 etag 값이 포함된 헤더를 보내고 If-None-Match 캐시 항목이 최신인 경우 서버는 전체 응답 대신 304 Not Modified를 반환합니다. 최소 API 앱에서 정책의 etag 값을 설정하는 방법은 다음과 같습니다.

app.MapGet("/etag", async (context) =>
{
    var etag = $"\"{Guid.NewGuid():n}\"";
    context.Response.Headers.ETag = etag;
    await Gravatar.WriteGravatar(context);

}).CacheOutput();

컨트롤러 기반 API에서 etag 값을 설정하는 방법은 다음과 같습니다.

[ApiController]
[Route("/[controller]")]
[OutputCache]
public class EtagController : ControllerBase
{
    public async Task GetAsync()
    {
        var etag = $"\"{Guid.NewGuid():n}\"";
        HttpContext.Response.Headers.ETag = etag;
        await Gravatar.WriteGravatar(HttpContext);
    }
}

캐시 유효성 재검사를 수행하는 또 다른 방법은 클라이언트에서 요청한 날짜와 비교하여 캐시 항목 생성 날짜를 확인하는 것입니다. 요청 헤더 If-Modified-Since가 제공되면 캐시된 항목이 오래되어 만료되지 않은 경우 출력 캐싱은 304를 반환합니다.

캐시 유효성 재검사는 클라이언트에서 보낸 이러한 헤더에 대한 응답으로 자동으로 수행됩니다. 출력 캐싱을 사용하도록 설정하는 것 외에도 이 동작을 사용하도록 설정하기 위해 서버에 특별한 구성이 필요하지 않습니다.

태그를 사용하여 캐시 항목 제거

태그를 사용하여 엔드포인트 그룹을 식별하고 그룹에 대한 모든 캐시 항목을 제거할 수 있습니다. 예를 들어 다음 최소 API 코드는 URL이 "블로그"로 시작하는 엔드포인트 쌍을 만들고 "tag-blog"에 태그를 지정합니다.

app.MapGet("/blog", Gravatar.WriteGravatar)
    .CacheOutput(builder => builder.Tag("tag-blog"));
app.MapGet("/blog/post/{id}", Gravatar.WriteGravatar)
    .CacheOutput(builder => builder.Tag("tag-blog"));

다음 코드는 컨트롤러 기반 API의 엔드포인트에 태그를 할당하는 방법을 보여 줍니다.

[ApiController]
[Route("/[controller]")]
[OutputCache(Tags = new[] { "tag-blog", "tag-all" })]
public class TagEndpointController : ControllerBase
{
    public async Task GetAsync()
    {
        await Gravatar.WriteGravatar(HttpContext);
    }
}

경로로 시작하는 엔드포인트에 대한 태그를 할당하는 다른 방법은 해당 경로가 blog 있는 모든 엔드포인트에 적용되는 기본 정책을 정의하는 것입니다. 다음 코드는 이 작업을 수행하는 방법을 보여줍니다.

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder
        .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
        .Tag("tag-blog"));
    options.AddBasePolicy(builder => builder.Tag("tag-all"));
    options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
    options.AddPolicy("NoCache", builder => builder.NoCache());
    options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});

최소 API 앱의 또 다른 대안은 다음을 호출 MapGroup하는 것입니다.

var blog = app.MapGroup("blog")
    .CacheOutput(builder => builder.Tag("tag-blog"));
blog.MapGet("/", Gravatar.WriteGravatar);
blog.MapGet("/post/{id}", Gravatar.WriteGravatar);

앞의 태그 할당 예제에서 두 엔드포인트는 모두 tag-blog 태그로 식별됩니다. 그런 다음 해당 태그를 참조하는 단일 문을 사용하여 해당 엔드포인트에 대한 캐시 항목을 제거할 수 있습니다.

app.MapPost("/purge/{tag}", async (IOutputCacheStore cache, string tag) =>
{
    await cache.EvictByTagAsync(tag, default);
});

이 코드를 사용하면 HTTP POST 요청이 전송되어 https://localhost:<port>/purge/tag-blog 이러한 엔드포인트에 대한 캐시 항목을 제거합니다.

모든 엔드포인트에 대한 모든 캐시 항목을 제거할 수 있습니다. 이렇게 하려면 다음 코드와 같이 모든 엔드포인트에 대한 기본 정책을 만듭니다.

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder
        .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
        .Tag("tag-blog"));
    options.AddBasePolicy(builder => builder.Tag("tag-all"));
    options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
    options.AddPolicy("NoCache", builder => builder.NoCache());
    options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});

이 기본 정책을 사용하면 "tag-all" 태그를 사용하여 캐시의 모든 항목을 제거할 수 있습니다.

리소스 잠금 사용 안 함

기본적으로 리소스 잠금은 캐시 스탬프 및 썬더링 무리의 위험을 완화하기 위해 활성화됩니다. 자세한 내용은 출력 캐싱을 참조하세요.

리소스 잠금을 사용하지 않도록 설정하려면 다음 예제와 같이 정책을 만드는 동안 SetLocking(false)을 호출합니다.

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder
        .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
        .Tag("tag-blog"));
    options.AddBasePolicy(builder => builder.Tag("tag-all"));
    options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
    options.AddPolicy("NoCache", builder => builder.NoCache());
    options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});

다음 예제에서는 최소 API 앱에서 엔드포인트에 대한 잠금 해제 정책을 선택합니다.

app.MapGet("/nolock", Gravatar.WriteGravatar)
    .CacheOutput("NoLock");

컨트롤러 기반 API에서 특성을 사용하여 정책을 선택합니다.

[ApiController]
[Route("/[controller]")]
[OutputCache(PolicyName = "NoLock")]
public class NoLockController : ControllerBase
{
    public async Task GetAsync()
    {
        await Gravatar.WriteGravatar(HttpContext);
    }
}

제한

OutputCacheOptions의 다음 속성을 사용하면 모든 엔드포인트에 적용되는 제한을 구성할 수 있습니다.

  • SizeLimit - 캐시 저장소의 최대 크기입니다. 이 제한에 도달하면 이전 항목이 제거될 때까지 새 응답이 캐시되지 않습니다. 기본값은 100MB입니다.
  • MaximumBodySize - 응답 본문이 이 제한을 초과하면 캐시되지 않습니다. 기본값은 64MB입니다.
  • DefaultExpirationTimeSpan - 정책에서 지정하지 않은 경우 적용되는 만료 기간입니다. 기본값은 60초입니다.

캐시 스토리지

IOutputCacheStore는 저장소에 사용됩니다. 기본적으로 MemoryCache와 함께 사용됩니다. 캐시된 응답은 프로세스에 저장되므로 각 서버에는 서버 프로세스가 다시 시작될 때마다 손실되는 별도의 캐시가 있습니다.

Redis cache

대안은 Redis 캐시를 사용하는 것입니다. Redis 캐시는 개별 서버 프로세스보다 오래 실행되는 공유 캐시를 통해 서버 노드 간에 일관성을 제공합니다. 출력 캐싱에 Redis를 사용하려면 다음을 수행합니다.

  • Microsoft.AspNetCore.OutputCaching.StackExchangeRedis NuGet 패키지를 설치합니다.

  • 호출 builder.Services.AddStackExchangeRedisOutputCache (아님AddStackExchangeRedisCache)하고 Redis 서버를 가리키는 연결 문자열 제공합니다.

    예시:

    builder.Services.AddStackExchangeRedisOutputCache(options =>
    {
        options.Configuration = 
            builder.Configuration.GetConnectionString("MyRedisConStr");
        options.InstanceName = "SampleInstance";
    });
    
    builder.Services.AddOutputCache(options =>
    {
        options.AddBasePolicy(builder => 
            builder.Expire(TimeSpan.FromSeconds(10)));
    });
    
    • options.Configuration- 온-프레미스 Redis 서버 또는 Azure Cache for Redis와 같은 호스트된 제품에 대한 연결 문자열. 예를 들어 <instance_name>.redis.cache.windows.net:6380,password=<password>,ssl=True,abortConnect=False Azure cache for Redis의 경우입니다.
    • options.InstanceName - 선택 사항으로, 캐시에 대한 논리 파티션을 지정합니다.

    구성 옵션은 Redis 기반 분산 캐싱 옵션과 동일합니다.

출력 캐싱에는 IDistributedCache를 사용하지 않는 것이 좋습니다. IDistributedCache에는 태그 지정에 필요한 원자성 기능이 없습니다. 기본 스토리지 메커니즘에 직접 종속성을 사용하여 Redis에 대한 기본 제공 지원을 사용하거나 사용자 지정 IOutputCacheStore 구현을 만드는 것이 좋습니다.

참고 항목

이 문서에서는 ASP.NET Core 앱에서 출력 캐싱 미들웨어를 구성하는 방법을 설명합니다. 출력 캐싱에 대한 소개는 출력 캐싱을 참조하세요.

출력 캐싱 미들웨어는 최소 API, 컨트롤러가 있는 Web API, MVC 및 Razor Pages와 같은 모든 유형의 ASP.NET Core 앱에서 사용할 수 있습니다. 샘플 앱은 최소 API이지만 보여 주는 모든 캐싱 기능은 다른 앱 유형에서도 지원됩니다.

앱에 미들웨어 추가

AddOutputCache를 호출하여 서비스 컬렉션에 출력 캐싱 미들웨어를 추가합니다.

UseOutputCache를 호출하여 요청 처리 파이프라인에 미들웨어를 추가합니다.

참고 항목

  • CORS 미들웨어를 사용하는 앱에서는 UseCors 이후에 UseOutputCache를 호출해야 합니다.
  • Razor Pages 앱 및 컨트롤러가 있는 앱에서 UseRouting 이후에 UseOutputCache를 호출해야 합니다.
  • AddOutputCacheUseOutputCache를 호출하면 캐싱 동작이 시작되지 않으므로 캐싱을 사용할 수 있습니다. 다음 섹션과 같이 캐싱 응답 데이터를 구성해야 합니다.

하나의 엔드포인트 또는 페이지 구성

최소 API 앱의 경우 다음 예제와 같이 CacheOutput을 호출하거나 [OutputCache] 특성을 적용하여 캐싱을 수행하도록 엔드포인트를 구성합니다.

app.MapGet("/cached", Gravatar.WriteGravatar).CacheOutput();
app.MapGet("/attribute", [OutputCache] (context) => 
    Gravatar.WriteGravatar(context));

컨트롤러가 있는 앱의 경우 작업 메서드에 [OutputCache] 특성을 적용합니다. Razor Pages 앱의 경우 Razor 페이지 클래스에 특성을 적용합니다.

여러 엔드포인트 또는 페이지 구성

여러 엔드포인트에 적용되는 캐싱 구성을 지정하기 위해 호출 AddOutputCache 할 때 정책을 만듭니다. 특정 엔드포인트에 대해 정책을 선택할 수 있지만 기본 정책은 엔드포인트 컬렉션에 대한 기본 캐싱 구성을 제공합니다.

다음 강조 표시된 코드는 만료 시간이 10초인 앱의 모든 엔드포인트에 대한 캐싱을 구성합니다. 만료 시간을 지정하지 않으면 기본값은 1분입니다.

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => 
        builder.Expire(TimeSpan.FromSeconds(10)));
    options.AddPolicy("Expire20", builder => 
        builder.Expire(TimeSpan.FromSeconds(20)));
    options.AddPolicy("Expire30", builder => 
        builder.Expire(TimeSpan.FromSeconds(30)));
});

다음 강조 표시된 코드는 각각 다른 만료 시간을 지정하는 두 개의 정책을 만듭니다. 선택한 엔드포인트는 20초 만료를 사용할 수 있으며 다른 엔드포인트는 30초 만료를 사용할 수 있습니다.

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => 
        builder.Expire(TimeSpan.FromSeconds(10)));
    options.AddPolicy("Expire20", builder => 
        builder.Expire(TimeSpan.FromSeconds(20)));
    options.AddPolicy("Expire30", builder => 
        builder.Expire(TimeSpan.FromSeconds(30)));
});

CacheOutput 메서드를 호출하거나 [OutputCache] 특성을 사용할 때 엔드포인트에 대한 정책을 선택할 수 있습니다.

app.MapGet("/20", Gravatar.WriteGravatar).CacheOutput("Expire20");
app.MapGet("/30", [OutputCache(PolicyName = "Expire30")] (context) => 
    Gravatar.WriteGravatar(context));

컨트롤러가 있는 앱의 경우 작업 메서드에 [OutputCache] 특성을 적용합니다. Razor Pages 앱의 경우 Razor 페이지 클래스에 특성을 적용합니다.

기본 출력 캐싱 정책

기본적으로 출력 캐싱은 다음 규칙을 따릅니다.

  • HTTP 200 응답만 캐시됩니다.
  • HTTP GET 또는 HEAD 요청만 캐시됩니다.
  • 쿠키를 설정하는 응답은 캐시되지 않습니다.
  • 인증된 요청에 대한 응답은 캐시되지 않습니다.

다음 코드는 모든 기본 캐싱 규칙을 앱의 모든 엔드포인트에 적용합니다.

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder.Cache());
});

기본 정책 재정의

다음 코드에서는 기본값 규칙을 재정의하는 방법을 보여 줍니다. 다음 사용자 지정 정책 코드에서 강조 표시된 줄은 HTTP POST 메서드 및 HTTP 301 응답에 대한 캐싱을 사용하도록 설정합니다.

using Microsoft.AspNetCore.OutputCaching;
using Microsoft.Extensions.Primitives;

namespace OCMinimal;

public sealed class MyCustomPolicy : IOutputCachePolicy
{
    public static readonly MyCustomPolicy Instance = new();

    private MyCustomPolicy()
    {
    }

    ValueTask IOutputCachePolicy.CacheRequestAsync(
        OutputCacheContext context, 
        CancellationToken cancellationToken)
    {
        var attemptOutputCaching = AttemptOutputCaching(context);
        context.EnableOutputCaching = true;
        context.AllowCacheLookup = attemptOutputCaching;
        context.AllowCacheStorage = attemptOutputCaching;
        context.AllowLocking = true;

        // Vary by any query by default
        context.CacheVaryByRules.QueryKeys = "*";

        return ValueTask.CompletedTask;
    }

    ValueTask IOutputCachePolicy.ServeFromCacheAsync
        (OutputCacheContext context, CancellationToken cancellationToken)
    {
        return ValueTask.CompletedTask;
    }

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

        // Verify existence of cookie headers
        if (!StringValues.IsNullOrEmpty(response.Headers.SetCookie))
        {
            context.AllowCacheStorage = false;
            return ValueTask.CompletedTask;
        }

        // Check response code
        if (response.StatusCode != StatusCodes.Status200OK && 
            response.StatusCode != StatusCodes.Status301MovedPermanently)
        {
            context.AllowCacheStorage = false;
            return ValueTask.CompletedTask;
        }

        return ValueTask.CompletedTask;
    }

    private static bool AttemptOutputCaching(OutputCacheContext context)
    {
        // Check if the current request fulfills the requirements
        // to be cached
        var request = context.HttpContext.Request;

        // Verify the method
        if (!HttpMethods.IsGet(request.Method) && 
            !HttpMethods.IsHead(request.Method) && 
            !HttpMethods.IsPost(request.Method))
        {
            return false;
        }

        // Verify existence of authorization headers
        if (!StringValues.IsNullOrEmpty(request.Headers.Authorization) || 
            request.HttpContext.User?.Identity?.IsAuthenticated == true)
        {
            return false;
        }

        return true;
    }
}

이 사용자 지정 정책을 사용하려면 명명된 정책을 만듭니다.

builder.Services.AddOutputCache(options =>
{
    options.AddPolicy("CachePost", MyCustomPolicy.Instance);
});

엔드포인트에 대해 명명된 정책을 선택합니다.

app.MapPost("/cachedpost", Gravatar.WriteGravatar)
    .CacheOutput("CachePost");

대체 기본 정책 재정의

또는 DI(종속성 주입)를 사용하여 사용자 지정 정책 클래스를 다음과 같이 변경하여 인스턴스를 초기화합니다.

  • 프라이빗 생성자 대신 퍼블릭 생성자입니다.
  • 사용자 지정 정책 클래스에서 Instance 속성을 제거합니다.

예시:

public sealed class MyCustomPolicy2 : IOutputCachePolicy
{

    public MyCustomPolicy2()
    {
    }

클래스의 나머지는 이전에 표시된 것과 동일합니다. 다음 예제와 같이 사용자 지정 정책을 추가합니다.

builder.Services.AddOutputCache(options =>
{
    options.AddPolicy("CachePost", builder => 
        builder.AddPolicy<MyCustomPolicy2>(), true);
});

앞의 코드는 DI를 사용하여 사용자 지정 정책 클래스의 인스턴스를 만듭니다. 생성자의 모든 퍼블릭 인수가 확인됩니다.

사용자 지정 정책을 기본 정책으로 사용하는 경우 기본 정책이 적용되어야 하는 엔드포인트에서 인수 없이 OutputCache()를 호출하지 마세요. OutputCache()를 호출하면 엔드포인트에 기본 정책이 추가됩니다.

캐시 경로 지정

기본적으로 URL의 모든 부분은 캐시 항목, 즉 스키마, 호스트, 포트, 경로 및 쿼리 문자열의 키로 포함됩니다. 그러나 캐시 키를 명시적으로 제어할 수 있습니다. 예를 들어 culture 쿼리 문자열의 각 고유 값에 대해서만 고유한 응답을 반환하는 엔드포인트가 있다고 가정합니다. 다른 쿼리 문자열과 같은 URL의 다른 부분의 변형으로 인해 캐시 항목이 달라져서는 안 됩니다. 다음 강조 표시된 코드와 같이 정책에서 이러한 규칙을 지정할 수 있습니다.

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder
        .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
        .Tag("tag-blog"));
    options.AddBasePolicy(builder => builder.Tag("tag-all"));
    options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
    options.AddPolicy("NoCache", builder => builder.NoCache());
    options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});

그런 다음, 엔드포인트에 대한 VaryByQuery 정책을 선택할 수 있습니다.

app.MapGet("/query", Gravatar.WriteGravatar).CacheOutput("Query");

다음은 캐시 키를 제어하기 위한 몇 가지 옵션입니다.

  • SetVaryByQuery - 캐시 키에 추가할 하나 이상의 쿼리 문자열 이름을 지정합니다.

  • SetVaryByHeader - 캐시 키에 추가할 HTTP 헤더를 하나 이상 지정합니다.

  • VaryByValue- 캐시 키에 추가할 값을 지정합니다. 다음 예제에서는 현재 서버 시간(초)이 홀수인지 짝수인지를 나타내는 값을 사용합니다. 새 응답은 초 수가 홀수에서 짝수 또는 홀수로 가는 경우에만 생성됩니다.

    app.MapGet("/varybyvalue", Gravatar.WriteGravatar)
        .CacheOutput(c => c.VaryByValue((context) => 
            new KeyValuePair<string, string>(
                "time", (DateTime.Now.Second % 2)
                    .ToString(CultureInfo.InvariantCulture))));
    

OutputCacheOptions.UseCaseSensitivePaths를 사용하여 키의 경로 부분이 대/소문자를 구분하도록 지정합니다. 기본값은 대/소문자 구분입니다.

자세한 옵션은 OutputCachePolicyBuilder 클래스를 참조하세요.

캐시 유효성 재검사

캐시 유효성 재검사로서 서버가 전체 응답 본문 대신 304 Not Modified HTTP 상태 코드를 반환할 수 있게 됩니다. 이 상태 코드는 요청에 대한 응답이 클라이언트가 이전에 받은 응답과 변경되지 않음을 클라이언트에 알릴 수 있습니다.

다음 코드에서는 Etag 헤더를 사용하여 캐시 유효성 검사를 사용하도록 설정하는 방법을 보여 줍니다. 클라이언트가 이전 응답의 etag 값으로 If-None-Match 헤더를 보내고 캐시 항목이 최신인 경우 서버는 전체 응답 대신 304 수정되지 않음 을 반환합니다.

app.MapGet("/etag", async (context) =>
{
    var etag = $"\"{Guid.NewGuid():n}\"";
    context.Response.Headers.ETag = etag;
    await Gravatar.WriteGravatar(context);

}).CacheOutput();

캐시 유효성 재검사를 수행하는 또 다른 방법은 클라이언트에서 요청한 날짜와 비교하여 캐시 항목 생성 날짜를 확인하는 것입니다. 요청 헤더 If-Modified-Since가 제공되면 캐시된 항목이 오래되어 만료되지 않은 경우 출력 캐싱은 304를 반환합니다.

캐시 유효성 재검사는 클라이언트에서 보낸 이러한 헤더에 대한 응답으로 자동으로 수행됩니다. 출력 캐싱을 사용하도록 설정하는 것 외에도 이 동작을 사용하도록 설정하기 위해 서버에 특별한 구성이 필요하지 않습니다.

태그를 사용하여 캐시 항목 제거

태그를 사용하여 엔드포인트 그룹을 식별하고 그룹에 대한 모든 캐시 항목을 제거할 수 있습니다. 예를 들어 다음 코드는 URL이 "blog"로 시작하는 엔드포인트 쌍을 만들고 "tag-blog"에 태그를 지정합니다.

app.MapGet("/blog", Gravatar.WriteGravatar)
    .CacheOutput(builder => builder.Tag("tag-blog"));
app.MapGet("/blog/post/{id}", Gravatar.WriteGravatar)
    .CacheOutput(builder => builder.Tag("tag-blog"));

동일한 엔드포인트 쌍에 태그를 할당하는 다른 방법은 blog로 시작하는 엔드포인트에 적용되는 기본 정책을 정의하는 것입니다.

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder
        .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
        .Tag("tag-blog"));
    options.AddBasePolicy(builder => builder.Tag("tag-all"));
    options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
    options.AddPolicy("NoCache", builder => builder.NoCache());
    options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});

또 다른 대안은 MapGroup을 호출하는 것입니다.

var blog = app.MapGroup("blog")
    .CacheOutput(builder => builder.Tag("tag-blog"));
blog.MapGet("/", Gravatar.WriteGravatar);
blog.MapGet("/post/{id}", Gravatar.WriteGravatar);

앞의 태그 할당 예제에서 두 엔드포인트는 모두 tag-blog 태그로 식별됩니다. 그런 다음 해당 태그를 참조하는 단일 문을 사용하여 해당 엔드포인트에 대한 캐시 항목을 제거할 수 있습니다.

app.MapPost("/purge/{tag}", async (IOutputCacheStore cache, string tag) =>
{
    await cache.EvictByTagAsync(tag, default);
});

이 코드를 사용하면 https://localhost:<port>/purge/tag-blog에 전송된 HTTP POST 요청이 이러한 엔드포인트에 대한 캐시 항목을 제거합니다.

모든 엔드포인트에 대한 모든 캐시 항목을 제거할 수 있습니다. 이렇게 하려면 다음 코드와 같이 모든 엔드포인트에 대한 기본 정책을 만듭니다.

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder
        .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
        .Tag("tag-blog"));
    options.AddBasePolicy(builder => builder.Tag("tag-all"));
    options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
    options.AddPolicy("NoCache", builder => builder.NoCache());
    options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});

이 기본 정책을 사용하면 "tag-all" 태그를 사용하여 캐시의 모든 항목을 제거할 수 있습니다.

리소스 잠금 사용 안 함

기본적으로 리소스 잠금은 캐시 스탬프 및 썬더링 무리의 위험을 완화하기 위해 활성화됩니다. 자세한 내용은 출력 캐싱을 참조하세요.

리소스 잠금을 사용하지 않도록 설정하려면 다음 예제와 같이 정책을 만드는 동안 SetLocking(false)을 호출합니다.

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder
        .With(c => c.HttpContext.Request.Path.StartsWithSegments("/blog"))
        .Tag("tag-blog"));
    options.AddBasePolicy(builder => builder.Tag("tag-all"));
    options.AddPolicy("Query", builder => builder.SetVaryByQuery("culture"));
    options.AddPolicy("NoCache", builder => builder.NoCache());
    options.AddPolicy("NoLock", builder => builder.SetLocking(false));
});

다음 예제에서는 엔드포인트에 대한 잠금 해제 정책을 선택합니다.

app.MapGet("/nolock", Gravatar.WriteGravatar)
    .CacheOutput("NoLock");

제한

OutputCacheOptions의 다음 속성을 사용하면 모든 엔드포인트에 적용되는 제한을 구성할 수 있습니다.

  • SizeLimit - 캐시 저장소의 최대 크기입니다. 이 제한에 도달하면 이전 항목이 제거될 때까지 새 응답이 캐시되지 않습니다. 기본값은 100MB입니다.
  • MaximumBodySize - 응답 본문이 이 제한을 초과하면 캐시되지 않습니다. 기본값은 64MB입니다.
  • DefaultExpirationTimeSpan - 정책에서 지정하지 않은 경우 적용되는 만료 기간입니다. 기본값은 60초입니다.

캐시 스토리지

IOutputCacheStore는 저장소에 사용됩니다. 기본적으로 MemoryCache와 함께 사용됩니다. 출력 캐싱에는 IDistributedCache를 사용하지 않는 것이 좋습니다. IDistributedCache에는 태그 지정에 필요한 원자성 기능이 없습니다. Redis와 같은 기본 저장소 메커니즘에 대한 직접 종속성을 사용하여 사용자 지정 IOutputCacheStore 구현을 만드는 것이 좋습니다. 또는 .NET 8에서 Redis 캐시에 대한 기본 제공 지원을 사용합니다.

참고 항목