다음을 통해 공유


.NET에서 캐싱

이 문서에서는 다양한 캐싱 메커니즘에 대해 알아봅니다. 캐싱은 중간 계층에 데이터를 저장하여 후속 데이터 검색을 더 빠르게 만드는 작업입니다. 개념적으로 캐싱은 성능 최적화 전략 및 디자인 고려 사항입니다. 캐싱은 자주 변경되지 않거나 검색 비용이 많이 드는 데이터를 더 쉽게 사용할 수 있게 함으로써 앱 성능을 크게 향상시킬 수 있습니다. 이 문서에서는 세 가지 캐싱 방법을 소개하고 각각에 대한 샘플 소스 코드를 제공합니다.

중요합니다

.NET 내에는 두 개의 MemoryCache 클래스가 System.Runtime.Caching 네임스페이스와 Microsoft.Extensions.Caching 네임스페이스에 각각 하나씩 있습니다.

이 문서에서는 캐싱에 중점을 두지만 NuGet 패키지는 System.Runtime.Caching 포함하지 않습니다. 모든 MemoryCache 참조는 Microsoft.Extensions.Caching 네임스페이스 내에 있습니다.

모든 Microsoft.Extensions.* 패키지는 DI(종속성 주입)를 지원합니다. IMemoryCache, HybridCacheIDistributedCache 인터페이스를 서비스로 사용할 수 있습니다.

메모리 내 캐싱

이 섹션에서는 Microsoft.Extensions.Caching.Memory 패키지에 대해 알아봅니다. 현재 구현된 IMemoryCacheConcurrentDictionary<TKey,TValue> 주위에 래퍼를 구현한 것으로, 기능이 풍부한 API를 제공합니다. 캐시 내의 항목은 ICacheEntry으로 표시되며 object에 해당하는 어떤 항목도 될 수 있습니다. 메모리 내 캐시 솔루션은 캐시된 데이터가 앱 프로세스에서 메모리를 임대하는 단일 서버에서 실행되는 앱에 적합합니다.

팁 (조언)

다중 서버 캐싱 시나리오의 경우 메모리 내 캐싱 대신 분산 캐싱 방법을 고려합니다.

메모리 내 캐싱 API

캐시의 소비자는 슬라이딩 및 절대 만료를 모두 제어할 수 있습니다.

만료를 설정하면 만료 시간 할당 내에 액세스하지 않으면 캐시의 항목이 제거 됩니다. 소비자는 MemoryCacheEntryOptions를 통해 캐시 항목을 제어할 수 있는 추가 옵션이 있습니다. 각각의 ICacheEntryMemoryCacheEntryOptions에 대한 만료 회피 기능을 제공하고, 우선 순위 설정은 CacheItemPriorityICacheEntry.Size의 제어를 포함합니다. 관련 확장 메서드는 다음과 같습니다.

메모리 내 캐시 예제

기본 IMemoryCache 구현을 사용하려면 확장 메서드를 AddMemoryCache 호출하여 필요한 모든 서비스를 DI에 등록합니다. 다음 코드 샘플에서 제네릭 호스트는 DI 기능을 노출하는 데 사용됩니다.

using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddMemoryCache();
using IHost host = builder.Build();

.NET 워크로드에 따라 IMemoryCache에 접근하는 방법이 다를 수 있으며, 그 예로 생성자 주입이 있습니다. 이 샘플에서는 IServiceProvider에서 host 인스턴스를 사용하고 제네릭 GetRequiredService<T>(IServiceProvider) 확장 메서드를 호출합니다.

IMemoryCache cache =
    host.Services.GetRequiredService<IMemoryCache>();

메모리 내 캐싱 서비스를 등록하고 DI를 통해 확인하면 캐싱을 시작할 준비가 된 것입니다. 이 샘플은 영어 알파벳의 'A'부터 'Z'까지의 각 문자를 순차적으로 반복합니다. 이 형식은 record AlphabetLetter 문자에 대한 참조를 유지하고 메시지를 생성합니다.

file record AlphabetLetter(char Letter)
{
    internal string Message =>
        $"The '{Letter}' character is the {Letter - 64} letter in the English alphabet.";
}

팁 (조언)

file 파일 내에서 정의되고 액세스되기 때문에 AlphabetLetter 형식에 액세스 한정자가 사용됩니다. 자세한 내용은 파일(C# 참조)을 참조하세요. 전체 소스 코드를 보려면 Program.cs 섹션을 참조하세요.

샘플에는 알파벳 문자를 반복하는 도우미 함수가 포함되어 있습니다.

static async ValueTask IterateAlphabetAsync(
    Func<char, Task> asyncFunc)
{
    for (char letter = 'A'; letter <= 'Z'; ++letter)
    {
        await asyncFunc(letter);
    }

    Console.WriteLine();
}

위의 C# 코드에서:

  • Func<char, Task> asyncFunc 반복에서 대기하여 현재 letter를 전달합니다.
  • 모든 문자가 처리되면 콘솔에 빈 줄이 기록됩니다.

캐시에 항목을 추가하려면 다음 중 하나 또는 Create API를 Set호출합니다.

var addLettersToCacheTask = IterateAlphabetAsync(letter =>
{
    MemoryCacheEntryOptions options = new()
    {
        AbsoluteExpirationRelativeToNow =
            TimeSpan.FromMilliseconds(MillisecondsAbsoluteExpiration)
    };

    _ = options.RegisterPostEvictionCallback(OnPostEviction);

    AlphabetLetter alphabetLetter =
        cache.Set(
            letter, new AlphabetLetter(letter), options);

    Console.WriteLine($"{alphabetLetter.Letter} was cached.");

    return Task.Delay(
        TimeSpan.FromMilliseconds(MillisecondsDelayAfterAdd));
});
await addLettersToCacheTask;

위의 C# 코드에서:

  • 변수 addLettersToCacheTaskIterateAlphabetAsync에게 위임되고 대기됩니다.
  • Func<char, Task> asyncFunc 람다로 주장된다.
  • 현재 시점을 기준으로 MemoryCacheEntryOptions가 절대 만료로 인스턴스화됩니다.
  • 퇴거 후 콜백이 등록됩니다.
  • AlphabetLetter 개체가 인스턴스화된 후, Setletteroptions와 함께 전달됩니다.
  • 문자는 캐시되는 것으로 콘솔에 기록됩니다.
  • 마지막으로 하나가 Task.Delay 반환됩니다.

알파벳의 각 문자에 대해 캐시 항목은 만료 및 제거 후 콜백으로 작성됩니다.

제거 후 콜백은 콘솔에 제거된 값의 세부 정보를 기록합니다.

static void OnPostEviction(
    object key, object? letter, EvictionReason reason, object? state)
{
    if (letter is AlphabetLetter alphabetLetter)
    {
        Console.WriteLine($"{alphabetLetter.Letter} was evicted for {reason}.");
    }
};

이제 캐시가 채워졌으므로 IterateAlphabetAsync에 대한 다른 호출이 대기됩니다. 그러나 이번에는 IMemoryCache.TryGetValue를 호출합니다.

var readLettersFromCacheTask = IterateAlphabetAsync(letter =>
{
    if (cache.TryGetValue(letter, out object? value) &&
        value is AlphabetLetter alphabetLetter)
    {
        Console.WriteLine($"{letter} is still in cache. {alphabetLetter.Message}");
    }

    return Task.CompletedTask;
});
await readLettersFromCacheTask;

cacheletter 키를 포함하고 있으며, 만약 valueAlphabetLetter의 인스턴스일 경우, 콘솔에 기록됩니다. letter 키가 캐시에 없으면 제거되고 제거 후 콜백이 호출되었습니다.

추가 확장 메서드

IMemoryCacheGetOrCreateAsync를 비롯한 많은 편리한 확장 메서드가 비동기로 함께 제공됩니다.

전부 합치세요

전체 샘플 앱 소스 코드는 최상위 프로그램이며 두 개의 NuGet 패키지가 필요합니다.

using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddMemoryCache();
using IHost host = builder.Build();

IMemoryCache cache =
    host.Services.GetRequiredService<IMemoryCache>();

const int MillisecondsDelayAfterAdd = 50;
const int MillisecondsAbsoluteExpiration = 750;

static void OnPostEviction(
    object key, object? letter, EvictionReason reason, object? state)
{
    if (letter is AlphabetLetter alphabetLetter)
    {
        Console.WriteLine($"{alphabetLetter.Letter} was evicted for {reason}.");
    }
};

static async ValueTask IterateAlphabetAsync(
    Func<char, Task> asyncFunc)
{
    for (char letter = 'A'; letter <= 'Z'; ++letter)
    {
        await asyncFunc(letter);
    }

    Console.WriteLine();
}

var addLettersToCacheTask = IterateAlphabetAsync(letter =>
{
    MemoryCacheEntryOptions options = new()
    {
        AbsoluteExpirationRelativeToNow =
            TimeSpan.FromMilliseconds(MillisecondsAbsoluteExpiration)
    };

    _ = options.RegisterPostEvictionCallback(OnPostEviction);

    AlphabetLetter alphabetLetter =
        cache.Set(
            letter, new AlphabetLetter(letter), options);

    Console.WriteLine($"{alphabetLetter.Letter} was cached.");

    return Task.Delay(
        TimeSpan.FromMilliseconds(MillisecondsDelayAfterAdd));
});
await addLettersToCacheTask;

var readLettersFromCacheTask = IterateAlphabetAsync(letter =>
{
    if (cache.TryGetValue(letter, out object? value) &&
        value is AlphabetLetter alphabetLetter)
    {
        Console.WriteLine($"{letter} is still in cache. {alphabetLetter.Message}");
    }

    return Task.CompletedTask;
});
await readLettersFromCacheTask;

await host.RunAsync();

file record AlphabetLetter(char Letter)
{
    internal string Message =>
        $"The '{Letter}' character is the {Letter - 64} letter in the English alphabet.";
}

MillisecondsDelayAfterAdd 값과 MillisecondsAbsoluteExpiration 값을 조정하여 캐시된 항목의 만료 및 제거와 관련된 동작 변화를 관찰할 수 있습니다. 다음은 이 코드를 실행하는 샘플 출력입니다. (.NET 이벤트의 비결정적 특성으로 인해 출력이 다를 수 있습니다.)

A was cached.
B was cached.
C was cached.
D was cached.
E was cached.
F was cached.
G was cached.
H was cached.
I was cached.
J was cached.
K was cached.
L was cached.
M was cached.
N was cached.
O was cached.
P was cached.
Q was cached.
R was cached.
S was cached.
T was cached.
U was cached.
V was cached.
W was cached.
X was cached.
Y was cached.
Z was cached.

A was evicted for Expired.
C was evicted for Expired.
B was evicted for Expired.
E was evicted for Expired.
D was evicted for Expired.
F was evicted for Expired.
H was evicted for Expired.
K was evicted for Expired.
L was evicted for Expired.
J was evicted for Expired.
G was evicted for Expired.
M was evicted for Expired.
N was evicted for Expired.
I was evicted for Expired.
P was evicted for Expired.
R was evicted for Expired.
O was evicted for Expired.
Q was evicted for Expired.
S is still in cache. The 'S' character is the 19 letter in the English alphabet.
T is still in cache. The 'T' character is the 20 letter in the English alphabet.
U is still in cache. The 'U' character is the 21 letter in the English alphabet.
V is still in cache. The 'V' character is the 22 letter in the English alphabet.
W is still in cache. The 'W' character is the 23 letter in the English alphabet.
X is still in cache. The 'X' character is the 24 letter in the English alphabet.
Y is still in cache. The 'Y' character is the 25 letter in the English alphabet.
Z is still in cache. The 'Z' character is the 26 letter in the English alphabet.

절대 만료(MemoryCacheEntryOptions.AbsoluteExpirationRelativeToNow)가 설정되었으므로 캐시된 모든 항목은 결국 제거됩니다.

작업 서비스 캐싱

데이터를 캐싱하기 위한 일반적인 전략 중 하나는 캐시를 소비하는 데이터 서비스와 독립적으로 업데이트하는 것입니다. 작업자 서비스 템플릿은 다른 애플리케이션 코드에서 독립적으로(또는 백그라운드에서) 실행되므로 BackgroundService 좋은 예입니다. 애플리케이션이 구현 IHostedService을 호스트하는 실행을 시작하면 해당 구현(이 경우 BackgroundService "작업자")이 동일한 프로세스에서 실행되기 시작합니다. 이러한 호스티드 서비스는 확장 메서드를 통해 AddHostedService<THostedService>(IServiceCollection) DI에 싱글톤으로 등록됩니다. 다른 서비스는 모든 서비스 수명 동안 DI에 등록할 수 있습니다.

중요합니다

서비스 수명 주기를 파악하는 것이 중요합니다. 모든 메모리 내 캐싱 서비스를 등록하기 위해 호출 AddMemoryCache 하면 서비스가 싱글톤으로 등록됩니다.

사진 서비스 시나리오

HTTP를 통해 액세스할 수 있는 타사 API를 사용하는 사진 서비스를 개발하고 있다고 상상해 보십시오. 이 사진 데이터는 자주 변경되지 않지만 많은 데이터가 있습니다. 각 사진은 간단한 record다음으로 표시됩니다.

namespace CachingExamples.Memory;

public readonly record struct Photo(
    int AlbumId,
    int Id,
    string Title,
    string Url,
    string ThumbnailUrl);

다음 예제에서는 여러 서비스가 DI에 등록되는 것을 볼 수 있습니다. 각 서비스에는 단일 책임이 있습니다.

using CachingExamples.Memory;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddMemoryCache();
builder.Services.AddHttpClient<CacheWorker>();
builder.Services.AddHostedService<CacheWorker>();
builder.Services.AddScoped<PhotoService>();
builder.Services.AddSingleton(typeof(CacheSignal<>));

using IHost host = builder.Build();

await host.StartAsync();

위의 C# 코드에서:

지정된 PhotoService 조건(또는 filter)에 맞는 사진을 가져오는 책임이 있습니다.

using Microsoft.Extensions.Caching.Memory;

namespace CachingExamples.Memory;

public sealed class PhotoService(
        IMemoryCache cache,
        CacheSignal<Photo> cacheSignal,
        ILogger<PhotoService> logger)
{
    public async IAsyncEnumerable<Photo> GetPhotosAsync(Func<Photo, bool>? filter = default)
    {
        try
        {
            await cacheSignal.WaitAsync();

            Photo[] photos =
                (await cache.GetOrCreateAsync(
                    "Photos", _ =>
                    {
                        logger.LogWarning("This should never happen!");

                        return Task.FromResult(Array.Empty<Photo>());
                    }))!;

            // If no filter is provided, use a pass-thru.
            filter ??= _ => true;

            foreach (Photo photo in photos)
            {
                if (!default(Photo).Equals(photo) && filter(photo))
                {
                    yield return photo;
                }
            }
        }
        finally
        {
            cacheSignal.Release();
        }
    }
}

위의 C# 코드에서:

  • 생성자에는 IMemoryCache, CacheSignal<Photo>, 및 ILogger.
  • 메서드: GetPhotosAsync
    • Func<Photo, bool> filter 매개 변수를 정의하고 IAsyncEnumerable<Photo>을 반환합니다.
    • _cacheSignal.WaitAsync()를 호출하고 해제될 때까지 기다립니다. 이렇게 하면 캐시에 액세스하기 전에 캐시가 채워지게 합니다.
    • 캐시에 있는 모든 사진을 비동기적으로 가져오는 호출 _cache.GetOrCreateAsync()입니다.
    • 인수는 factory 경고를 기록하고 빈 사진 배열을 반환합니다. 이렇게 하면 안 됩니다.
    • 캐시에 있는 각 사진은 yield return을 사용하여 반복되고, 필터링되며, 구체화됩니다.
    • 마지막으로 캐시 신호가 다시 설정됩니다.

이 서비스의 소비자는 자유롭게 메서드를 호출 GetPhotosAsync 하고 그에 따라 사진을 처리할 수 있습니다. 필요한 HttpClient 것은 없습니까? 캐시에 사진이 포함되어 있습니다.

비동기 신호는 제네릭 형식 제한 싱글톤 내의 캡슐화된 SemaphoreSlim 인스턴스를 기반으로 합니다. CacheSignal<T>은/는 SemaphoreSlim 인스턴스에 의존합니다.

namespace CachingExamples.Memory;

public sealed class CacheSignal<T>
{
    private readonly SemaphoreSlim _semaphore = new(1, 1);

    /// <summary>
    /// Exposes a <see cref="Task"/> that represents the asynchronous wait operation.
    /// When signaled (consumer calls <see cref="Release"/>), the 
    /// <see cref="Task.Status"/> is set as <see cref="TaskStatus.RanToCompletion"/>.
    /// </summary>
    public Task WaitAsync() => _semaphore.WaitAsync();

    /// <summary>
    /// Exposes the ability to signal the release of the <see cref="WaitAsync"/>'s operation.
    /// Callers who were waiting, will be able to continue.
    /// </summary>
    public void Release() => _semaphore.Release();
}

위의 C# 코드에서 데코레이터 패턴은 인스턴스 SemaphoreSlim를 래핑하는 데 사용됩니다. CacheSignal<T>이 싱글톤으로 등록되어 있으므로, 모든 제네릭 형식의 서비스 수명 전반에 걸쳐 사용할 수 있습니다. 이 경우에는 Photo. 캐시 시딩을 알리는 역할을 담당합니다.

CacheWorker 하위 클래스입니다.BackgroundService

using System.Net.Http.Json;
using Microsoft.Extensions.Caching.Memory;

namespace CachingExamples.Memory;

public sealed class CacheWorker(
    ILogger<CacheWorker> logger,
    HttpClient httpClient,
    CacheSignal<Photo> cacheSignal,
    IMemoryCache cache) : BackgroundService
{
    private readonly TimeSpan _updateInterval = TimeSpan.FromHours(3);

    private bool _isCacheInitialized = false;

    private const string Url = "https://jsonplaceholder.typicode.com/photos";

    public override async Task StartAsync(CancellationToken cancellationToken)
    {
        await cacheSignal.WaitAsync();
        await base.StartAsync(cancellationToken);
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            logger.LogInformation("Updating cache.");

            try
            {
                Photo[]? photos =
                    await httpClient.GetFromJsonAsync<Photo[]>(
                        Url, stoppingToken);

                if (photos is { Length: > 0 })
                {
                    cache.Set("Photos", photos);
                    logger.LogInformation(
                        "Cache updated with {Count:#,#} photos.", photos.Length);
                }
                else
                {
                    logger.LogWarning(
                        "Unable to fetch photos to update cache.");
                }
            }
            finally
            {
                if (!_isCacheInitialized)
                {
                    cacheSignal.Release();
                    _isCacheInitialized = true;
                }
            }

            try
            {
                logger.LogInformation(
                    "Will attempt to update the cache in {Hours} hours from now.",
                    _updateInterval.Hours);

                await Task.Delay(_updateInterval, stoppingToken);
            }
            catch (OperationCanceledException)
            {
                logger.LogWarning("Cancellation acknowledged: shutting down.");
                break;
            }
        }
    }
}

위의 C# 코드에서:

  • 생성자에는 ILogger, HttpClient, 및 IMemoryCache.
  • _updateInterval 3시간 동안 정의됩니다.
  • 메서드: ExecuteAsync
    • 앱이 실행되는 동안 반복합니다.
    • HTTP 요청을 수행하고 "https://jsonplaceholder.typicode.com/photos"응답을 개체 배열 Photo 로 매핑합니다.
    • 사진 배열은 IMemoryCache 내에서 "Photos" 키 아래에 배치됩니다.
    • _cacheSignal.Release()가 호출되면 신호를 기다리고 있던 모든 소비자가 해제됩니다.
    • 업데이트 간격이 Task.Delay 지정된 경우 호출이 대기됩니다.
    • 3시간 동안 지연된 후 캐시가 다시 업데이트됩니다.

동일한 프로세스의 소비자는 IMemoryCache에게 사진을 요청할 수 있지만, CacheWorker는 캐시를 업데이트할 책임이 있습니다.

하이브리드 캐싱

라이브러리는 HybridCache 기존 캐싱 API와 일반적인 문제를 해결하는 동시에 메모리 내 및 분산 캐싱의 이점을 결합합니다. .NET 9 HybridCache 에서 도입된 이 API는 캐싱 구현을 간소화하고 스탬피드 보호 및 구성 가능한 serialization과 같은 기본 제공 기능을 포함하는 통합 API를 제공합니다.

주요 기능

HybridCacheIMemoryCacheIDistributedCache를 따로 사용하는 것에 비해 몇 가지 이점을 제공합니다.

  • 2단계 캐싱: 메모리 내(L1) 및 분산(L2) 캐시 계층을 모두 자동으로 관리합니다. 데이터는 먼저 메모리 내 캐시에서 빠른 속도로 검색된 다음, 필요한 경우 분산 캐시에서, 마지막으로 원본에서 검색됩니다.
  • 스탬피드 보호: 여러 동시 요청이 동일한 비용이 드는 작업을 실행하지 못하도록 방지합니다. 하나의 요청만 데이터를 가져오는 반면 다른 요청은 결과를 기다립니다.
  • 구성 가능한 serialization: JSON(기본값), protobuf 및 XML을 비롯한 여러 serialization 형식을 지원합니다.
  • 태그 기반 무효화: 효율적인 일괄 처리 무효화를 위해 태그를 사용하여 관련 캐시 항목을 그룹화합니다.
  • 간소화된 API: 메서드는 GetOrCreateAsync 캐시 누락, serialization 및 스토리지를 자동으로 처리합니다.

HybridCache를 사용하는 경우

다음 경우에 사용하는 것이 HybridCache 좋습니다.

  • 다중 서버 환경에서 로컬(메모리 내) 및 분산 캐싱이 모두 필요합니다.
  • 캐시 스탬피드 시나리오로부터 보호받고 싶습니다.
  • 수동으로 IMemoryCacheIDistributedCache를 조정하는 것보다 간소화된 API를 선호합니다.
  • 관련 항목에 대한 태그 기반 캐시 무효화가 필요합니다.

팁 (조언)

간단한 캐싱 요구 사항이 있는 단일 서버 애플리케이션의 경우 메모리 내 캐싱 으로 충분할 수 있습니다. 스탬피드 보호 또는 태그 기반 무효화가 필요 없는 다중 서버 애플리케이션의 경우 분산 캐싱을 고려합니다.

HybridCache 설정

HybridCache를 사용하려면 Microsoft.Extensions.Caching.Hybrid NuGet 패키지를 설치하세요.

dotnet add package Microsoft.Extensions.Caching.Hybrid

DI에 서비스를 등록하려면 HybridCache를 호출하여 AddHybridCache를 사용하세요.

var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHybridCache();

위의 코드는 기본 옵션으로 등록됩니다 HybridCache . 전역 옵션을 구성할 수도 있습니다.

var builderWithOptions = Host.CreateApplicationBuilder(args);
builderWithOptions.Services.AddHybridCache(options =>
{
    options.MaximumPayloadBytes = 1024 * 1024; // 1 MB
    options.MaximumKeyLength = 1024;
    options.DefaultEntryOptions = new HybridCacheEntryOptions
    {
        Expiration = TimeSpan.FromMinutes(5),
        LocalCacheExpiration = TimeSpan.FromMinutes(2)
    };
});

기본 사용법

ko-KR: HybridCache와 상호 작용하는 기본 방법은 GetOrCreateAsync입니다. 이 메서드는 지정된 키를 가진 항목에 대한 캐시를 확인하고, 찾을 수 없는 경우 팩터리 메서드를 호출하여 데이터를 검색합니다.

async Task<WeatherData> GetWeatherDataAsync(HybridCache cache, string city)
{
    return await cache.GetOrCreateAsync(
        $"weather:{city}",
        async cancellationToken =>
        {
            // Simulate fetching from an external API
            await Task.Delay(100, cancellationToken);
            return new WeatherData(city, 72, "Sunny");
        }
    );
}

위의 C# 코드에서:

  • 이 메서드는 GetOrCreateAsync 고유 키와 팩터리 메서드를 사용합니다.
  • 데이터가 캐시에 없으면 팩터리 메서드를 호출하여 검색합니다.
  • 데이터는 메모리 내 및 분산 캐시 모두에 자동으로 저장됩니다.
  • 하나의 동시 요청만 팩터리 메서드를 실행합니다. 다른 사용자는 결과를 기다립니다.

입력 옵션

다음을 사용하여 HybridCacheEntryOptions특정 캐시 항목에 대한 전역 기본값을 재정의할 수 있습니다.

async Task<WeatherData> GetWeatherWithOptionsAsync(HybridCache cache, string city)
{
    var entryOptions = new HybridCacheEntryOptions
    {
        Expiration = TimeSpan.FromMinutes(10),
        LocalCacheExpiration = TimeSpan.FromMinutes(5)
    };

    return await cache.GetOrCreateAsync(
        $"weather:{city}",
        async cancellationToken => new WeatherData(city, 72, "Sunny"),
        entryOptions
    );
}

항목 옵션을 사용하면 다음을 구성할 수 있습니다.

태그 기반 무효화

태그를 사용하면 관련 캐시 항목을 그룹화하고 함께 무효화할 수 있습니다. 이는 관련 데이터를 한 단위로 새로 고쳐야 하는 시나리오에 유용합니다.

async Task<CustomerData> GetCustomerAsync(HybridCache cache, int customerId)
{
    var tags = new[] { "customer", $"customer:{customerId}" };

    return await cache.GetOrCreateAsync(
        $"customer:{customerId}",
        async cancellationToken => new CustomerData(customerId, "John Doe", "john@example.com"),
        new HybridCacheEntryOptions { Expiration = TimeSpan.FromMinutes(30) },
        tags
    );
}

특정 태그를 사용하여 모든 항목을 무효화하려면 다음을 수행합니다.

async Task InvalidateCustomerCacheAsync(HybridCache cache, int customerId)
{
    await cache.RemoveByTagAsync($"customer:{customerId}");
}

여러 태그를 한 번에 무효화할 수도 있습니다.

async Task InvalidateAllCustomersAsync(HybridCache cache)
{
    await cache.RemoveByTagAsync(new[] { "customer", "orders" });
}

비고

태그 기반 무효화는 논리적 작업입니다. 캐시에서 값을 적극적으로 제거하지는 않지만 태그가 지정된 항목이 캐시 누락으로 처리되도록 합니다. 결국 항목은 구성된 수명에 따라 만료됩니다.

캐시 항목 제거

키별로 특정 캐시 항목을 제거하려면 다음 메서드를 RemoveAsync 사용합니다.

async Task RemoveWeatherDataAsync(HybridCache cache, string city)
{
    await cache.RemoveAsync($"weather:{city}");
}

캐시된 모든 항목을 무효화하려면 예약된 와일드카드 태그 "*"를 사용합니다.

async Task InvalidateAllCacheAsync(HybridCache cache)
{
    await cache.RemoveByTagAsync("*");
}

직렬화

분산 캐싱 시나리오의 HybridCache 경우 serialization이 필요합니다. 기본적으로 stringbyte[]를 내부적으로 처리하고, 다른 형식에 대해서는 System.Text.Json를 사용합니다. 특정 형식에 대해 사용자 지정 직렬 변환기를 구성하거나 범용 직렬 변환기를 사용할 수 있습니다.

// Custom serialization example
// Note: This requires implementing a custom IHybridCacheSerializer<T>
var builderWithSerializer = Host.CreateApplicationBuilder(args);
builderWithSerializer.Services.AddHybridCache(options =>
{
    options.DefaultEntryOptions = new HybridCacheEntryOptions
    {
        Expiration = TimeSpan.FromMinutes(10),
        LocalCacheExpiration = TimeSpan.FromMinutes(5)
    };
});
// To add a custom serializer, uncomment and provide your implementation:
// .AddSerializer<WeatherData, CustomWeatherDataSerializer>();

분산 캐시 구성

HybridCache 는 L2(분산) 캐시에 대해 구성된 IDistributedCache 구현을 사용합니다. 구성되지 않은 IDistributedCache에도 HybridCache은 여전히 메모리 내 캐싱 및 스탬피드 방지를 제공합니다. Redis를 분산 캐시로 추가하려면 다음을 수행합니다.

// Distributed cache with Redis
var builderWithRedis = Host.CreateApplicationBuilder(args);
builderWithRedis.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = "localhost:6379";
});
builderWithRedis.Services.AddHybridCache(options =>
{
    options.DefaultEntryOptions = new HybridCacheEntryOptions
    {
        Expiration = TimeSpan.FromMinutes(30),
        LocalCacheExpiration = TimeSpan.FromMinutes(5)
    };
});

분산 캐시 구현에 대한 자세한 내용은 분산 캐싱을 참조하세요.

분산 캐싱

일부 시나리오에서는 분산 캐시가 필요합니다. 예를 들어 여러 앱 서버가 있는 경우입니다. 분산 캐시는 메모리 내 캐싱 방법보다 더 높은 스케일 아웃을 지원합니다. 분산 캐시를 사용하면 캐시 메모리가 외부 프로세스로 오프로드되지만 추가 네트워크 I/O가 필요하고 약간의 대기 시간이 발생합니다(명목상인 경우에도).

분산 캐싱 추상화는 NuGet 패키지의 Microsoft.Extensions.Caching.Memory 일부이며 확장 메서드도 AddDistributedMemoryCache 있습니다.

주의

AddDistributedMemoryCache 는 개발 또는 테스트 시나리오에서만 사용해야 하며 실행 가능한 프로덕션 구현이 아닙니다 .

다음 패키지에서 사용할 수 있는 IDistributedCache의 어떤 구현이든 고려해 보세요.

분산 캐싱 API

분산 캐싱 API는 메모리 내 캐싱 API보다 약간 더 기본적입니다. 키-값 쌍은 좀 더 기본적입니다. 메모리 내 캐싱 키는 object을 기반으로 하며, 반면에 분산 키는 string입니다. 메모리 내 캐싱을 사용하면 값이 강력한 형식의 제네릭일 수 있지만 분산 캐싱의 값은 다음과 같이 byte[]유지됩니다. 즉, 다양한 구현이 강력한 형식의 제네릭 값을 노출하지는 않지만 구현 세부 정보입니다.

값 만들기

분산 캐시에 값을 만들려면 집합 API 중 하나를 호출합니다.

AlphabetLetter 메모리 내 캐시 예제의 레코드를 사용하여 객체를 JSON으로 직렬화한 다음 stringbyte[]로 인코딩할 수 있습니다.

DistributedCacheEntryOptions options = new()
{
    AbsoluteExpirationRelativeToNow =
        TimeSpan.FromMilliseconds(MillisecondsAbsoluteExpiration)
};

AlphabetLetter alphabetLetter = new(letter);
string json = JsonSerializer.Serialize(alphabetLetter);
byte[] bytes = Encoding.UTF8.GetBytes(json);

await cache.SetAsync(letter.ToString(), bytes, options);

메모리 내 캐싱과 마찬가지로 캐시 항목에는 캐시의 존재를 미세 조정하는 데 도움이 되는 옵션이 있을 수 있습니다. 이 경우 DistributedCacheEntryOptions.

확장 메서드 만들기

값을 만들기 위한 몇 가지 편의 기반 확장 메서드가 있습니다. 개체의 표현을 string로 인코딩하지 않도록 하는 데 이러한 메서드가 byte[]의 도움이 됩니다.

값 읽기

분산 캐시에서 값을 읽으려면 API 중 Get 하나를 호출합니다.

AlphabetLetter? alphabetLetter = null;
byte[]? bytes = await cache.GetAsync(letter.ToString());
if (bytes is { Length: > 0 })
{
    string json = Encoding.UTF8.GetString(bytes);
    alphabetLetter = JsonSerializer.Deserialize<AlphabetLetter>(json);
}

캐시 항목을 캐시에서 읽은 후에는 byte[]로부터 UTF8로 인코딩된 string 표현을 가져올 수 있습니다.

확장 메서드 읽기

값을 읽기 위한 몇 가지 편의 기반 확장 메서드가 있습니다. 이러한 메서드는 byte[]string 객체 표현으로 디코딩되지 않도록 하는 데 도움이 됩니다.

값 업데이트

단일 API 호출을 사용하여 분산 캐시의 값을 업데이트할 수 있는 방법은 없습니다. 대신 값은 새로 고침 API 중 하나를 사용하여 슬라이딩 만료를 재설정할 수 있습니다.

실제 값을 업데이트해야 하는 경우 값을 삭제한 다음 다시 추가해야 합니다.

값 삭제

분산 캐시의 값을 삭제하려면 API 중 Remove 하나를 호출합니다.

팁 (조언)

이러한 API의 동기 버전이 있지만 분산 캐시의 구현이 네트워크 I/O에 의존한다는 사실을 고려합니다. 이러한 이유로 일반적으로 비동기 API를 사용하는 것이 좋습니다.

참고하십시오