ASP.NET Core의 메모리 내 캐시Cache in-memory in ASP.NET Core

작성자: Rick Anderson, John LuoSteve SmithBy Rick Anderson, John Luo, and Steve Smith

예제 코드 살펴보기 및 다운로드(다운로드 방법)View or download sample code (how to download)

캐싱 기본 사항Caching basics

캐싱은 콘텐츠를 생성하기 위해 필요한 작업을 줄임으로써 응용 프로그램의 성능 및 확장성을 크게 향상시킵니다.Caching can significantly improve the performance and scalability of an app by reducing the work required to generate content. 캐싱은 자주 변경되지 않는 데이터에 가장 효과가 좋습니다.Caching works best with data that changes infrequently. 캐싱은 원본에서 가져와서 반환하는 것보다 더 빠르게 반환할 수 있는 데이터의 복사본을 만듭니다.Caching makes a copy of data that can be returned much faster than from the original source. 응용 프로그램이 캐시된 데이터에 의존하지 않도록 만들어야 하고 테스트해야 합니다.You should write and test your app to never depend on cached data.

ASP.NET Core는 몇 가지 다른 종류의 캐시를 지원합니다.ASP.NET Core supports several different caches. 가장 간단한 캐시는 웹 서버의 메모리에 저장된 캐시를 나타내는 IMemoryCache를 기반으로 합니다.The simplest cache is based on the IMemoryCache, which represents a cache stored in the memory of the web server. 다수의 서버로 구성된 서버 팜에서 실행되는 응용 프로그램이 메모리 내 캐시를 사용할 경우에는 세션이 고정적인지 확인해야 합니다.Apps which run on a server farm of multiple servers should ensure that sessions are sticky when using the in-memory cache. 고정 세션은 클라이언트의 이어지는 후속 요청이 모두 동일한 서버로 전달되도록 보장해줍니다.Sticky sessions ensure that subsequent requests from a client all go to the same server. 예를 들어 Azure 웹 앱은 이어지는 모든 후속 요청을 동일한 서버로 라우트하기 위해서 응용 프로그램 요청 라우팅(ARR)을 사용합니다.For example, Azure Web apps use Application Request Routing (ARR) to route all subsequent requests to the same server.

웹 팜에서 비-고정 세션을 사용할 경우에는 캐시 일관성 문제가 발생하지 않도록 분산 캐시가 필요합니다.Non-sticky sessions in a web farm require a distributed cache to avoid cache consistency problems. 일부 응용 프로그램의 경우, 분산 캐시가 메모리 내 캐시보다 더 높은 규모 확장을 지원할 수 있습니다. For some apps, a distributed cache can support higher scale out than an in-memory cache. 분산 캐시를 사용하면 캐시 메모리를 외부 프로세스에서 관리합니다.Using a distributed cache offloads the cache memory to an external process.

IMemoryCache 캐시는 캐시 우선 순위CacheItemPriority.NeverRemove로 설정되지 않은 캐시 항목을 메모리 부하에 따라 제거합니다.The IMemoryCache cache will evict cache entries under memory pressure unless the cache priority is set to CacheItemPriority.NeverRemove. CacheItemPriority를 설정하면 메모리에 부하가 걸렸을 때 캐시가 항목을 제거하는 우선 순위를 조정할 수 있습니다.You can set the CacheItemPriority to adjust the priority with which the cache evicts items under memory pressure.

메모리 내 캐시는 모든 개체를 저장할 수 있는 반면, 분산 캐시 인터페이스는 byte[]만 저장할 수 있습니다.The in-memory cache can store any object; the distributed cache interface is limited to byte[].

IMemoryCache 사용하기Using IMemoryCache

메모리 내 캐시는 응용 프로그램에서 종속성 주입을 통해서 참조되는 서비스입니다.In-memory caching is a service that's referenced from your app using Dependency Injection. ConfigureServices에서 AddMemoryCache를 호출합니다.Call AddMemoryCache in ConfigureServices:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMemoryCache();
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseMvcWithDefaultRoute();
    }
}

그런 다음 생성자에서 IMemoryCache의 인스턴스를 요청합니다.Request the IMemoryCache instance in the constructor:

public class HomeController : Controller
{
    private IMemoryCache _cache;

    public HomeController(IMemoryCache memoryCache)
    {
        _cache = memoryCache;
    }

IMemoryCache NuGet 패키지를 필요 Microsoft.Extensions.Caching.Memory합니다.IMemoryCache requires NuGet package Microsoft.Extensions.Caching.Memory.

IMemoryCache NuGet 패키지를 필요 Microsoft.Extensions.Caching.Memory에서 사용 가능한 되는 Microsoft.AspNetCore.All metapackage합니다.IMemoryCache requires NuGet package Microsoft.Extensions.Caching.Memory, which is avaiable in the Microsoft.AspNetCore.All metapackage.

IMemoryCache NuGet 패키지를 필요 Microsoft.Extensions.Caching.Memory에서 사용 가능한 되는 Microsoft.AspNetCore.App metapackage합니다.IMemoryCache requires NuGet package Microsoft.Extensions.Caching.Memory, which is avaiable in the Microsoft.AspNetCore.App metapackage.

다음 코드는 TryGetValue를 이용해서 시간이 캐시되어 있는지 확인합니다.The following code uses TryGetValue to check if a time is in the cache. 만약 시간이 캐시되어 있지 않다면 새로운 항목이 생성되고 Set을 통해서 캐시에 추가됩니다.If a time isn't cached, a new entry is created and added to the cache with Set.

public IActionResult CacheTryGetValueSet()
{
    DateTime cacheEntry;

    // Look for cache key.
    if (!_cache.TryGetValue(CacheKeys.Entry, out cacheEntry))
    {
        // Key not in cache, so get data.
        cacheEntry = DateTime.Now;

        // Set cache options.
        var cacheEntryOptions = new MemoryCacheEntryOptions()
            // Keep in cache for this time, reset time if accessed.
            .SetSlidingExpiration(TimeSpan.FromSeconds(3));

        // Save data in cache.
        _cache.Set(CacheKeys.Entry, cacheEntry, cacheEntryOptions);
    }

    return View("Cache", cacheEntry);
}

현재 시간과 캐시된 시간이 표시됩니다.The current time and the cached time are displayed:

@model DateTime?

<div>
    <h2>Actions</h2>
    <ul>
        <li><a asp-controller="Home" asp-action="CacheTryGetValueSet">TryGetValue and Set</a></li>
        <li><a asp-controller="Home" asp-action="CacheGet">Get</a></li>
        <li><a asp-controller="Home" asp-action="CacheGetOrCreate">GetOrCreate</a></li>
        <li><a asp-controller="Home" asp-action="CacheGetOrCreateAsync">GetOrCreateAsync</a></li>
        <li><a asp-controller="Home" asp-action="CacheRemove">Remove</a></li>
    </ul>
</div>

<h3>Current Time: @DateTime.Now.TimeOfDay.ToString()</h3>
<h3>Cached Time: @(Model == null ? "No cached entry found" : Model.Value.TimeOfDay.ToString())</h3>

제한 시간 안에 요청이 전달되는 동안에는 (그리고 메모리 부족으로 제거되지 않은 경우에는) 캐시된 DateTime 값이 캐시에 남아 있습니다.The cached DateTime value remains in the cache while there are requests within the timeout period (and no eviction due to memory pressure). 다음 그림은 현재 시간과 캐시에서 조회한 그보다 오래된 시간을 보여줍니다.The following image shows the current time and an older time retrieved from the cache:

두 개의 서로 다른 시간을 표시하는 Index 뷰

다음 코드는 GetOrCreateGetOrCreateAsync 를 이용해서 데이터를 캐시합니다.The following code uses GetOrCreate and GetOrCreateAsync to cache data.

public IActionResult CacheGetOrCreate()
{
    var cacheEntry = _cache.GetOrCreate(CacheKeys.Entry, entry =>
    {
        entry.SlidingExpiration = TimeSpan.FromSeconds(3);
        return DateTime.Now;
    });

    return View("Cache", cacheEntry);
}

public async Task<IActionResult> CacheGetOrCreateAsync()
{
    var cacheEntry = await
        _cache.GetOrCreateAsync(CacheKeys.Entry, entry =>
    {
        entry.SlidingExpiration = TimeSpan.FromSeconds(3);
        return Task.FromResult(DateTime.Now);
    });

    return View("Cache", cacheEntry);
}

다음 코드는 Get 을 호출해서 캐시된 시간을 가져옵니다.The following code calls Get to fetch the cached time:

public IActionResult CacheGet()
{
    var cacheEntry = _cache.Get<DateTime?>(CacheKeys.Entry);
    return View("Cache", cacheEntry);
}

캐시 메서드에 대한 설명은 IMemoryCache 메서드CacheExtensions 메서드를 참고하시기 바랍니다.See IMemoryCache methods and CacheExtensions methods for a description of the cache methods.

MemoryCacheEntryOptions 사용하기Using MemoryCacheEntryOptions

다음 예제에서는:The following sample:

  • 절대 만료 시간을 설정합니다.Sets the absolute expiration time. 절대 만료 시간은 항목이 캐시될 수 있는 최대 시간으로, 슬라이딩 만료가 지속적으로 갱신될 경우 항목이 이전 상태로 지속되는 것을 방지합니다.This is the maximum time the entry can be cached and prevents the item from becoming too stale when the sliding expiration is continuously renewed.
  • 슬라이딩 만료 시간을 설정합니다.Sets a sliding expiration time. 상대 만료 시계는 캐시된 항목에 접근하는 요청에 따라 재설정됩니다.Requests that access this cached item will reset the sliding expiration clock.
  • 캐시 우선 순위를 CacheItemPriority.NeverRemove로 설정합니다.Sets the cache priority to CacheItemPriority.NeverRemove.
  • 캐시에서 항목이 제거된 후에 호출되는 PostEvictionDelegate 를 설정합니다. 콜백은 캐시에서 항목을 제거하는 코드와 다른 스레드에서 실행됩니다.Sets a PostEvictionDelegate that will be called after the entry is evicted from the cache. 콜백 캐시에서 항목을 제거 하는 코드에서 다른 스레드에서 실행 됩니다.The callback is run on a different thread from the code that removes the item from the cache.
public IActionResult CreateCallbackEntry()
{
    var cacheEntryOptions = new MemoryCacheEntryOptions()
        // Pin to cache.
        .SetPriority(CacheItemPriority.NeverRemove)
        // Add eviction callback
        .RegisterPostEvictionCallback(callback: EvictionCallback, state: this);

    _cache.Set(CacheKeys.CallbackEntry, DateTime.Now, cacheEntryOptions);

    return RedirectToAction("GetCallbackEntry");
}

public IActionResult GetCallbackEntry()
{
    return View("Callback", new CallbackViewModel
    {
        CachedTime = _cache.Get<DateTime?>(CacheKeys.CallbackEntry),
        Message = _cache.Get<string>(CacheKeys.CallbackMessage)
    });
}

public IActionResult RemoveCallbackEntry()
{
    _cache.Remove(CacheKeys.CallbackEntry);
    return RedirectToAction("GetCallbackEntry");
}

private static void EvictionCallback(object key, object value,
    EvictionReason reason, object state)
{
    var message = $"Entry was evicted. Reason: {reason}.";
    ((HomeController)state)._cache.Set(CacheKeys.CallbackMessage, message);
}

캐시 종속성Cache dependencies

다음 예제는 종속적인 항목을 만료할 경우 캐시 항목을 만료하는 방법을 보여줍니다.The following sample shows how to expire a cache entry if a dependent entry expires. 캐시된 항목에 CancellationChangeToken이 추가됩니다.A CancellationChangeToken is added to the cached item. CancellationTokenSourceCancel이 호출되면 캐시된 두 항목 모두 제거됩니다.When Cancel is called on the CancellationTokenSource, both cache entries are evicted.

public IActionResult CreateDependentEntries()
{
    var cts = new CancellationTokenSource();
    _cache.Set(CacheKeys.DependentCTS, cts);

    using (var entry = _cache.CreateEntry(CacheKeys.Parent))
    {
        // expire this entry if the dependant entry expires.
        entry.Value = DateTime.Now;
        entry.RegisterPostEvictionCallback(DependentEvictionCallback, this);

        _cache.Set(CacheKeys.Child,
            DateTime.Now,
            new CancellationChangeToken(cts.Token));
    }

    return RedirectToAction("GetDependentEntries");
}

public IActionResult GetDependentEntries()
{
    return View("Dependent", new DependentViewModel
    {
        ParentCachedTime = _cache.Get<DateTime?>(CacheKeys.Parent),
        ChildCachedTime = _cache.Get<DateTime?>(CacheKeys.Child),
        Message = _cache.Get<string>(CacheKeys.DependentMessage)
    });
}

public IActionResult RemoveChildEntry()
{
    _cache.Get<CancellationTokenSource>(CacheKeys.DependentCTS).Cancel();
    return RedirectToAction("GetDependentEntries");
}

private static void DependentEvictionCallback(object key, object value,
    EvictionReason reason, object state)
{
    var message = $"Parent entry was evicted. Reason: {reason}.";
    ((HomeController)state)._cache.Set(CacheKeys.DependentMessage, message);
}

CancellationTokenSource를 사용하면 다수의 캐시 항목을 그룹으로 제거할 수 있습니다.Using a CancellationTokenSource allows multiple cache entries to be evicted as a group. 위의 코드의 using 패턴을 사용하면,using 블록 안에서 생성된 캐시 항목들이 트리거 및 만료 설정을 상속받습니다.With the using pattern in the code above, cache entries created inside the using block will inherit triggers and expiration settings.

추가 참고 사항Additional notes

  • 캐시 항목을 다시 채우기 위해서 콜백을 사용할 때:When using a callback to repopulate a cache item:

    • 다수의 요청이 캐시된 키 값으로 빈 값을 얻을 수도 있는데, 이는 콜백이 완료되지 않았기 때문입니다.Multiple requests can find the cached key value empty because the callback hasn't completed.
    • 이로 인해 다수의 스레드가 캐시된 항목을 다시 채울 수 있습니다.This can result in several threads repopulating the cached item.
  • 한 캐시 항목을 사용해서 다른 캐시 항목을 만들 때, 하위 항목은 부모 항목의 만료 토큰 및 시간 기반의 만료 설정을 복사합니다.When one cache entry is used to create another, the child copies the parent entry's expiration tokens and time-based expiration settings. 하위 항목은 부모 항목을 수동으로 제거하거나 갱신하더라도 만료되지 않습니다.The child isn't expired by manual removal or updating of the parent entry.

추가 자료Additional resources