소프트웨어 개발 과정에서 애플리케이션의 성능은 항상 중요한 관심사입니다. 성능을 개선하는 방법은 다양하지만, 그중에서도 현대 클라우드 애플리케이션에서 가장 널리 사용되는 패턴 중 하나가 바로 캐시 어사이드(Cache-Aside) 패턴입니다. 이 글에서는 캐시 어사이드 패턴의 개념을 간략히 살펴보고, ASP.NET Core를 활용한 구현 방법을 단계별로 소개하겠습니다.
캐시 어사이드 패턴이란?
캐시 어사이드 패턴은 매우 직관적인 패턴으로, 데이터 원본(Data Source)에서 필요한 시점에 캐시로 데이터를 로드하는 것이 유일한 목적입니다. 이를 통해 캐시의 데이터와 실제 데이터 저장소 간의 일관성을 유지할 수 있습니다.
이 패턴의 동작 방식은 다음과 같습니다.
- 애플리케이션이 데이터가 필요하면 먼저 캐시를 확인합니다.
- 캐시에 해당 데이터가 존재하면(캐시 히트), 캐시에서 데이터를 가져와 사용합니다.
- 캐시에 데이터가 없다면(캐시 미스), 데이터 원본에서 데이터를 조회합니다.
아래 다이어그램은 이러한 흐름을 시각적으로 보여줍니다.

애플리케이션에서 값이 변경될 때는 반드시 캐시 객체를 무효화(invalidate)해야 합니다.

특히 캐시를 무효화하는 순서가 매우 중요합니다. 반드시 데이터 원본을 먼저 업데이트한 후 캐시에서 항목을 제거해야 합니다. 만약 캐시를 먼저 제거하면, 클라이언트가 데이터 저장소가 업데이트되기 전에 이전 데이터를 조회할 수 있으며, 이는 데이터 저장소와 캐시 간의 불일치로 이어질 수 있습니다.
언제 이 패턴을 사용해야 할까?
- 필요할 때마다 데이터를 캐시에 로드하는 방식이므로, 리소스 수요가 예측하기 어려운 환경에 적합합니다.
- 읽기 스루(Read-Through) 및 쓰기 스루(Write-Through) 기능을 제공하지 않는 캐시를 사용하는 경우에도 활용할 수 있습니다.
참고: Read-Through와 Write-Through
- Read-Through: 캐시가 데이터베이스 앞단에 위치하여, 캐시 미스 발생 시 자동으로 데이터베이스에서 데이터를 로드해 캐시를 채우는 방식입니다.
- Write-Through: 캐시가 데이터베이스 앞단에 위치하여, 모든 데이터가 항상 캐시를 거쳐 메인 데이터베이스에 기록되는 방식입니다.
Azure 리소스 생성하기
앞서 설명한 것처럼 이 예제에는 데이터베이스(Azure SQL Server)와 캐시(Azure Redis Cache)가 필요합니다. 물론 본인에게 편리한 다른 데이터베이스나 캐시 서비스를 선택해도 무방합니다.
$resourceGroup="<Resource Group>"
$location="<location>"
$redisCacheName="<Redis cache name>"
$sqlServerName="<Azure SQL Server Name>"
$sqlDBName="<Azure SQL DB Name>"
$adminName="<admin name of SQL server>"
$adminPassword="<admin password of SQL Server>"
# 리소스 그룹 생성
az group create --name $resourceGroup --location $location
# Basic SKU로 Redis Cache 생성
az redis create --name $redisCacheName --resource-group $resourceGroup --location $location --sku Basic --vm-size c0
# SQL Server 생성
az sql server create -l $location -g $resourceGroup -n $sqlServerName -u $adminName -p $adminPassword
# Basic SKU로 SQL 데이터베이스 생성
az sql db create -g $resourceGroup -s $sqlServerName -n $sqlDBName --service-objective Basic
구현하기
이제 ASP.NET Core Web API 프로젝트를 생성하고, Redis 캐시와 Entity Framework Core에 필요한 NuGet 패키지를 추가하며 구현을 시작해 보겠습니다.
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
먼저 국가(Country) 모델 클래스를 생성합니다.
public class Country
{
public int Id { get; set; }
public string Name { get; set; }
public bool IsActive { get; set; }
}
다음으로 Startup 클래스의 ConfigureServices 메서드에서 EF Core와 Redis 캐시의 의존성을 등록합니다.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddDbContext<CountryContext>(optionsAction =>
optionsAction.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddStackExchangeRedisCache(setupAction =>
{
setupAction.Configuration = Configuration.GetConnectionString("RedisConnectionString");
});
}
그다음 appsettings.json 파일을 수정하여 Redis 캐시와 SQL 데이터베이스의 연결 문자열을 추가합니다.
"ConnectionStrings": {
"RedisConnectionString": "<Redis Cache ConnectionString>",
"DefaultConnection": "<SQL Server Connection string>"
}
이제 DbContext 클래스를 추가합니다.
public class CountryContext : DbContext
{
public DbSet<Country> Countries { get; set; }
public CountryContext(DbContextOptions dbContextOptions) : base(dbContextOptions)
{
}
}
GetCountries 메서드는 키를 사용하여 캐시에서 항목을 조회합니다. 일치하는 데이터가 있으면 즉시 반환하고, 없으면 데이터베이스에서 데이터를 가져온 뒤 캐시에 저장합니다. 여기서는 캐시된 항목이 5분 후 만료되도록 설정했습니다.
[Route("api/[controller]")]
[ApiController]
public class CountryController : ControllerBase
{
private readonly IDistributedCache cache;
private readonly CountryContext countryContext;
public CountryController(IDistributedCache cache, CountryContext countryContext)
{
this.cache = cache;
this.countryContext = countryContext;
}
// GET: api/<CountryController>
[HttpGet]
public async Task<IEnumerable<Country>> GetCountries()
{
var countriesCache = await cache.GetStringAsync("countries");
var value = (countriesCache == null) ? default : JsonConvert.DeserializeObject<IEnumerable<Country>>(countriesCache);
if (value == null)
{
var countries = countryContext.Countries.ToList();
if (countries != null && countries.Any())
{
await cache.SetStringAsync("Countries", JsonConvert.SerializeObject(countries), new DistributedCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5)
});
return countries;
}
}
return value;
}
}
AddCountries 메서드는 데이터베이스에 데이터를 추가하거나 업데이트할 때 캐시를 어떻게 무효화하는지 보여줍니다.
// POST api/<CountryController>
[HttpPost]
public async Task<ActionResult<string>> AddCountries([FromBody] Country country, CancellationToken cancellationToken)
{
if (country == null)
return BadRequest("country is null");
await countryContext.AddAsync(country);
await countryContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
await cache.RemoveAsync("countries", cancellationToken).ConfigureAwait(false);
return Ok("cache has been invalidated");
}
마무리
이번 글에서는 캐시 어사이드 패턴의 개념과 함께 ASP.NET Core와 Azure Redis Cache를 활용한 기본적인 구현 방법을 살펴보았습니다. 캐싱을 잘 활용하면 애플리케이션의 응답 속도와 확장성을 크게 향상시킬 수 있습니다. Happy Caching!
이 글이 도움이 되었다면 좋아요와 공유 부탁드립니다.