하이브리드 캐시(Hybrid Cache)란?
하이브리드 캐시는 데이터를 메모리 내(in-memory)와 외부 소스 양쪽에 캐싱할 수 있도록 지원하는 통합 라이브러리로, '다층 캐싱(multi-tier caching)'이라고도 부릅니다. 즉, 이 다층 캐싱은 기존의 IDistributedCache와 IMemoryCache를 대체하며, .NET에서 캐싱 사용을 단순화하는 것을 목표로 합니다. 기존의 분산 캐싱 방식에서는 데이터가 올바르게 저장되고 조회되도록 별도의 코드를 추가로 작성해야 했습니다. Microsoft.Extensions.Caching.Hybrid NuGet 패키지는 이러한 번거로움을 없애고 견고한 캐싱 솔루션을 손쉽게 구축할 수 있게 해주는 훌륭한 선택입니다. 이 글에서는 하이브리드 캐시 패키지의 모든 기능을 살펴보겠습니다.
패키지 주요 기능
- 확장 가능한 코드: 메모리 내 캐시를 위해 작성된 코드는 수정 없이 그대로 Redis, SQL Server 등 외부 캐싱 서버와 연동하는 데 재사용할 수 있습니다.
- 동시성 관리(Concurrency Management): 하이브리드 캐시를 사용하려면
HybridCache클래스를 의존성 서비스로 등록해야 합니다. 이 클래스는 특정 키에 대해 오직 하나의 인스턴스만 데이터를 조회하도록 보장하며, 동일한 항목에 대한 중복 요청이 발생하지 않도록 합니다. 나머지 동시 요청들은 해당 요청이 완료될 때까지 대기합니다. - 다중 소스 캐싱 / 1차·2차 소스 캐싱: 애플리케이션에서 여러 데이터 소스를 캐싱 대상으로 구성한 경우, 데이터는 모든 위치에 저장됩니다.
데이터를 조회할 때는 먼저 1차 소스(primary source)를 확인하고, 해당 데이터가 없으면 요청이 2차 소스(secondary source)로 전달됩니다. 아래 다이어그램에서 파란색이 시나리오 1, 빨간색이 시나리오 2에 해당합니다.
코드 설정
시작하려면 먼저 Microsoft.Extensions.Caching.Hybrid 패키지를 설치합니다.
Program.cs에 아래 코드를 작성하여 하이브리드 캐시를 추가합니다.
builder.Services.AddHybridCache();
하이브리드 캐시를 세부 구성하려면 AddHybridCache 안에 설정 액션(setup action) 형태로 아래 코드를 작성합니다. 예시는 다음과 같습니다.
builder.Services.AddHybridCache(options =>
{
options.ReportTagMetrics = true;
options.DefaultEntryOptions = new Microsoft.Extensions.Caching.Hybrid.HybridCacheEntryOptions
{
Expiration = TimeSpan.FromSeconds(30),
LocalCacheExpiration = TimeSpan.FromSeconds(30),
};
});
메모리 내 캐시 구현
위 의존성 서비스 구성만으로도 메모리 내 캐싱을 수행할 수 있습니다. 아래는 API 컨트롤러에서 애플리케이션 메모리에 키와 값을 설정하고 조회하는 샘플 코드입니다.
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Hybrid;
namespace HybridCacheDemoApplication.Controllers
{
[ApiController]
[Route("[controller]")]
public class CacheController : ControllerBase
{
private readonly HybridCache _cache;
const string cacheKey = "ping";
public CacheController(HybridCache cache)
{
_cache = cache;
}
[HttpGet(Name = "GetPingKey")]
public async Task<IActionResult> Get()
{
var keydata = await _cache.GetOrCreateAsync<string>(
cacheKey, // 캐시 항목의 고유 키
async cancel => await Task.FromResult("pong from get")
);
return Ok(new { message = $"{cacheKey} data: {keydata}" });
}
[HttpGet("set", Name = "SetPingKey")]
public async Task<IActionResult> Set()
{
await _cache.SetAsync<string>(cacheKey, "ping");
return Ok(new { message = $"{cacheKey} is set" });
}
}
}
이 코드가 어떻게 동작하는지 아래 표를 순서대로 확인해 보세요.
캐시 실행 데모 표
| 순서 | 엔드포인트 | 출력 | 설명 |
|---|---|---|---|
| 1 | https://localhost:5154/cache/ | { "message": "ping data: pong from get" } |
값이 설정되어 있지 않으면 이 엔드포인트는 캐시 값을 "pong from get"으로 저장하므로 "pong from get"이 출력됩니다. |
| 2 | https://localhost:5154/cache/set | { "message": "ping is set" } |
이 엔드포인트는 캐시에 값을 설정합니다. |
| 3 | https://localhost:5154/cache/ | { "message": "ping data: ping" } |
1단계에서 호출한 것과 동일한 엔드포인트지만, 이번에는 값이 이미 캐시에 존재합니다. |
캐시 키 삭제: 캐시 키를 제거하려면 아래 구현을 사용합니다.
[HttpGet("del", Name = "DeletePingKey")]
public async Task<IActionResult> RemoveKey()
{
await _cache.RemoveAsync(cacheKey); // 데이터 삭제
return Ok(new { message = $"{cacheKey} removed" });
}
Redis 서버를 활용한 분산 캐싱
분산 캐싱을 구현하려면 추가적인 의존성 서비스 구성이 필요합니다. 이 예제에서는 외부 캐싱 소스로 Redis 캐시를 사용합니다.
먼저 Microsoft.Extensions.Caching.StackExchangeRedis 패키지를 통해 Redis를 구성합니다.
설치 후 아래 코드를 추가하여 Redis를 등록합니다. 연결 문자열 형식은 {HOST_NAME}:{PORT_NUMBER},password={PASSWORD}입니다.
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration =
builder.Configuration.GetConnectionString("RedisConnectionString");
});
이제 https://localhost:5154/cache/set 을 호출하면 데이터가 별도의 코드 변경 없이 Redis 캐시에 자연스럽게 저장됩니다. Redis에서의 모습은 아래와 같습니다.

분산 캐싱 서버인 Redis에 대한 의존성만 추가했을 뿐인데, 기존 코드가 얼마나 매끄럽게 동작하는지 주목해 보세요.
직렬화(Serialization)
외부 캐싱 서버로 데이터를 전송할 때 이 패키지는 byte[], string, System.Text.Json을 사용합니다. AddHybridCache 의존성 서비스에 .AddSerializer() 또는 AddSerializerFactory() 메서드를 추가로 사용하면 직렬화 방식을 커스터마이징할 수 있습니다. 더 최적화된 직렬화기를 사용하면 애플리케이션 성능이 향상됩니다.
추가 커스터마이징
의존성 구성 시 플래그를 설정하여 캐시 서비스의 동작을 더욱 세밀하게 조정할 수 있습니다. 아래는 캐싱 시 압축을 비활성화하는 플래그 예제입니다.
참고: 이 방식은 모범 사례는 아니며, 압축이 필요하지 않거나 사용되지 않는 시나리오에 따라 적용 여부가 달라질 수 있습니다.
builder.Services.AddHybridCache(options =>
{
options.ReportTagMetrics = true;
options.DefaultEntryOptions = new Microsoft.Extensions.Caching.Hybrid.HybridCacheEntryOptions
{
Flags = Microsoft.Extensions.Caching.Hybrid.HybridCacheEntryFlags.DisableCompression
};
});
실험적 기능: 태그(Tags) - 상호 연관된 키 다루기
때로는 서로 연관된 수많은 키를 다뤄야 할 수 있습니다. 예시: 전자상거래 애플리케이션에서 특정 사용자의 장바구니 항목에 해당하는 여러 필드를 캐시했다고 가정해 보겠습니다. 이런 경우 태그를 만들어 키들을 그룹화할 수 있습니다.
아래 코드에서는 상호 연관된 키를 다루기 위해 "testdata"라는 태그를 생성했습니다. 시나리오별 그룹화 기준에 따라 두 개 이상의 태그를 추가할 수도 있습니다. 코드는 자명하지만, 궁금한 점이 있다면 댓글로 남겨주세요.
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Hybrid;
namespace HybridCacheDemoApplication.Controllers
{
[ApiController]
[Route("[controller]")]
public class CacheController : ControllerBase
{
private readonly HybridCache _cache;
const string cacheKey = "ping";
public CacheController(HybridCache cache)
{
_cache = cache;
}
[HttpGet(Name = "GetPingKey")]
public async Task<IActionResult> Get()
{
var keydata = await _cache.GetOrCreateAsync<string>(
cacheKey, // 캐시 항목의 고유 키
async cancel => await Task.FromResult("pong from get"),
tags: ["testdata"]
);
return Ok(new { message = $"{cacheKey} data: {keydata}" });
}
[HttpGet("set", Name = "SetPingKey")]
public async Task<IActionResult> Set()
{
await _cache.SetAsync<string>(cacheKey,
"ping",
tags: ["testdata"]
);
return Ok(new { message = $"{cacheKey} is set" });
}
[HttpGet("del", Name = "DeletePingKey")]
public async Task<IActionResult> RemoveKey()
{
await _cache.RemoveAsync(cacheKey);
return Ok(new { message = $"{cacheKey} removed" });
}
[HttpGet("delbytag", Name = "DeleteByTestTags")]
public async Task<IActionResult> RemoveKeyByTags()
{
await _cache.RemoveByTagAsync(["testdata"]);
return Ok(new { message = $"{cacheKey} removed" });
}
}
}
끝까지 읽어주셔서 감사합니다. 하이브리드 캐싱은 놀라운 이점을 제공하며, 여러분의 프로젝트에도 큰 도움이 되기를 바랍니다.