ASP.NET Core와 메모리 내 캐시
- 본 번역문서의 원문은 Cache in-memory in ASP.NET Core docs.microsoft.com 입니다.
캐싱 기본 사항
캐싱은 콘텐츠를 생성하기 위해 필요한 작업을 줄여줌으로써 응용 프로그램의 성능 및 확장성을 크게 향상시킵니다. 캐싱은 자주 변경되지 않는 데이터에 가장 효과가 좋습니다. 캐싱은 원본에서 가져와서 반환하는 것보다 더 빠르게 반환할 수 있는 데이터의 복사본을 만듭니다. 응용 프로그램이 캐시된 데이터에 의존하지 않도록 만들고 테스트를 해야 합니다.
ASP.NET Core는 몇 가지 다른 종류의 캐시를 지원합니다. 가장 간단한 캐시는 웹 서버의 메모리에 저장된 캐시를 나타내는 IMemoryCache를 기반으로 합니다. 다수의 서버로 구성된 서버 팜에서 실행되는 응용 프로그램이 메모리 내 캐시를 사용할 경우에는 세션이 고정적인지 확인해야 합니다. 고정 세션은 클라이언트의 이어지는 후속 요청이 모두 동일한 서버로 전달되도록 보장해줍니다. 예를 들어 Azure 웹 앱은 이어지는 모든 후속 요청을 동일한 서버로 라우트하기 위해서 응용 프로그램 요청 라우팅(ARR, Application Request Routing)을 사용합니다.
웹 팜에서 비-고정 세션을 사용할 경우에는 캐시 일관성 문제가 발생하지 않도록 분산 캐시를 사용해야 합니다. 일부 응용 프로그램의 경우, 분산 캐시가 메모리 내 캐시보다 더 높은 규모 확장을 지원할 수 있습니다. 분산 캐시를 사용하면 캐시 메모리를 외부 프로세스에서 관리합니다.
IMemoryCache
캐시는 캐시 우선 순위가 CacheItemPriority.NeverRemove
로 설정되지 않은 캐시 항목을 메모리 부하에 따라 제거합니다.
CacheItemPriority
를 설정하면 메모리에 부하가 걸렸을 때 캐시가 항목을 제거하는 우선 순위를 조정할 수 있습니다.
메모리 내 캐시는 모든 개체를 저장할 수 있는 반면, 분산 캐시 인터페이스는 byte[]
만 저장할 수 있습니다.
IMemoryCache 사용하기
메모리 내 캐시는 응용 프로그램에서 종속성 주입을 통해서 참조되는 서비스입니다.
먼저 ConfigureServices
에서 AddMemoryCache
를 호출합니다:
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMemoryCache();
services.AddMvc();
}
public void Configure(IApplicationBuilder app)
{
app.UseMvcWithDefaultRoute();
}
}
그런 다음 생성자에서 IMemoryCache
의 인스턴스를 요청합니다:
public class HomeController : Controller
{
private IMemoryCache _cache;
public HomeController(IMemoryCache memoryCache)
{
_cache = memoryCache;
}
IMemoryCache
를 사용하기 위해서는 "Microsoft.Extensions.Caching.Memory" NuGet 패키지가 필요합니다.
다음 코드는 TryGetValue를 이용해서 시간이 캐시되어 있는지 확인합니다. 만약 시간이 캐시되어 있지 않다면 새로운 항목을 생성하고 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);
}
현재 시간과 캐시된 시간을 표시합니다:
@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
값이 캐시에 남아 있습니다.
다음 그림은 현재 시간과 캐시에서 조회한 그보다 오래된 시간을 보여줍니다:
다음 코드는 GetOrCreate 및 GetOrCreateAsync를 이용해서 데이터를 캐시합니다.
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을 호출해서 캐시된 시간을 가져옵니다:
public IActionResult CacheGet()
{
var cacheEntry = _cache.Get<DateTime?>(CacheKeys.Entry);
return View("Cache", cacheEntry);
}
캐시 메서드에 대한 설명은 IMemoryCache 인터페이스의 메서드 및 CacheExtensions 클래스의 메서드를 참고하시기 바랍니다.
MemoryCacheEntryOptions 사용하기
다음 예제는:
- 절대 만료 시간을 설정합니다. 절대 만료 시간은 항목이 캐시될 수 있는 최대 시간으로, 슬라이딩 만료가 지속적으로 갱신될 경우 항목이 너무 낡아지는 것을 방지합니다.
- 슬라이딩 만료 시간을 설정합니다. 상대 만료 시계는 캐시된 항목에 접근하는 요청에 의해 재설정됩니다.
-
캐시 우선 순위를
CacheItemPriority.NeverRemove
로 설정합니다. - 캐시에서 항목이 제거된 후에 호출되는 PostEvictionDelegate를 설정합니다. 콜백은 캐시에서 항목을 제거하는 코드와 다른 스레드에서 실행됩니다.
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);
}
캐시 종속성
다음 예제는 종속적인 항목을 만료할 경우 캐시 항목을 만료하는 방법을 보여줍니다.
캐시된 항목에 CancellationChangeToken
이 추가되는데, 이 CancellationTokenSource
의 Cancel
이 호출되면 캐시된 두 항목이 모두 제거됩니다.
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
패턴을 사용하면, using
블록 안에서 생성된 캐시 항목들이 트리거 및 만료 설정을 상속받습니다.
추가 참고 사항
-
캐시 항목을 다시 채우기 위해서 콜백을 사용할 때:
- 다수의 요청이 캐시된 키 값으로 빈 값을 얻을 수도 있는데, 이는 콜백이 완료되지 않았기 때문입니다.
- 이로 인해 다수의 스레드가 캐시된 항목을 다시 채울 수 있습니다.
-
한 캐시 항목을 사용해서 다른 캐시 항목을 만들 때, 하위 항목은 부모 항목의 만료 토큰 및 시간 기반의 만료 설정을 복사합니다. 하위 항목은 부모 항목을 수동으로 제거하거나 갱신하더라도 만료되지 않습니다.
추가 자료
- ASP.NET Core와 메모리 내 캐시 2018-05-14 08:00
- ASP.NET Core에서 분산 캐시 사용하기 2018-05-21 08:00
- ASP.NET Core와 응답 캐싱 2018-05-28 08:00
- ASP.NET Core와 응답 캐싱 미들웨어 2018-06-04 08:00