이 글에서는 Azure Redis Cache의 기본 개념과 .NET Core Web API를 활용한 실제 구현 방법을 단계별로 자세히 살펴봅니다.
목차
- 소개
- 캐시란 무엇인가?
- 캐시의 종류
- Redis 캐시
- Azure Redis Cache 설정
- 단계별 구현
사전 준비 사항
- Visual Studio 2022
- Azure 계정
- .NET Core 6
소개
캐싱은 애플리케이션의 성능과 확장성을 크게 향상시킬 수 있어 최근 소프트웨어 업계에서 각광받고 있는 기술입니다. Gmail이나 Facebook 같은 웹 애플리케이션을 사용해 보면 얼마나 빠르게 반응하고 훌륭한 사용자 경험을 제공하는지 실감할 수 있습니다. 인터넷 사용자가 폭발적으로 증가하면서 네트워크 트래픽과 요청량이 많은 애플리케이션일수록 성능과 응답성을 개선하기 위한 다양한 대책이 필요합니다. 바로 이러한 문제를 해결하는 강력한 솔루션이 캐싱이며, 이것이 캐싱이 주목받는 핵심 이유입니다.
캐시란 무엇인가?
캐시(Cache)는 자주 접근하는 데이터를 임시 저장소에 보관하는 메모리 저장소입니다. 캐시를 활용하면 불필요한 데이터베이스 조회를 줄여 성능을 획기적으로 개선할 수 있으며, 자주 사용되는 데이터를 캐시 메모리에 저장해 두고 빠르게 재사용할 수 있습니다.


위 이미지에는 두 가지 시나리오가 있습니다. 하나는 캐시를 사용하지 않는 경우이고, 다른 하나는 캐시를 사용하는 경우입니다. 캐시를 사용하지 않으면 사용자가 데이터를 요청할 때마다 매번 데이터베이스에 접근하게 되므로 시간 복잡도가 증가하고 성능이 저하됩니다. 특히 모든 사용자에게 동일하게 제공되는 정적 데이터라면 더욱 비효율적입니다. 반면 캐시를 사용하면 첫 번째 사용자만 데이터베이스에 접근해 데이터를 가져와 캐시 메모리에 저장하고, 이후 나머지 사용자들은 데이터베이스에 불필요하게 접근하지 않고도 캐시에서 데이터를 바로 가져올 수 있습니다.
캐시의 종류
.NET Core에서 지원하는 캐싱 방식은 기본적으로 두 가지입니다.
- 인메모리 캐싱(In-Memory Caching)
- 분산 캐싱(Distributed Caching)
인메모리 캐시를 사용하면 데이터가 애플리케이션 서버의 메모리에 저장되며, 필요할 때마다 해당 메모리에서 데이터를 가져와 사용합니다. 반면 분산 캐싱은 Redis를 비롯한 여러 서드파티 솔루션을 활용합니다. 이 글에서는 Redis Cache를 중점적으로 살펴보고 .NET Core에서 어떻게 동작하는지 자세히 알아보겠습니다.
분산 캐싱(Distributed Caching)

- 분산 캐싱에서는 데이터가 여러 서버에 걸쳐 저장되고 공유됩니다.
- 멀티 테넌트 애플리케이션 환경에서 여러 서버 간 부하를 분산 관리함으로써 애플리케이션의 확장성과 성능을 손쉽게 개선할 수 있습니다.
- 향후 특정 서버에 장애가 발생해 재시작하더라도 필요에 따라 여러 서버가 역할을 분담하고 있기 때문에 애플리케이션에는 영향이 없습니다.
Redis는 현재 많은 기업들이 애플리케이션의 성능과 확장성을 개선하기 위해 사용하는 가장 인기 있는 캐시 솔루션입니다. 지금부터 Redis와 그 활용 방법을 하나씩 살펴보겠습니다.
Redis 캐시(Redis Cache)
- Redis는 데이터베이스로 사용할 수 있는 오픈 소스(BSD 라이선스) 인메모리 데이터 구조 저장소입니다.
- 주로 자주 사용되는 데이터나 정적 데이터를 캐시 내부에 저장하고, 사용자 요구에 따라 활용 및 유지하는 용도로 사용됩니다.
- Redis에는 List, Set, Hashing, Stream 등 데이터를 저장할 수 있는 다양한 자료구조가内置되어 있어 상황에 맞게 선택해 사용할 수 있습니다.
Azure Redis Cache 설정
1단계
Azure 포털에 로그인합니다.
2단계
마켓플레이스에서 'Azure Cache for Redis'를 검색하여 엽니다.

3단계
[만들기(Create)]를 클릭하고 필요한 정보를 입력합니다.




4단계
앞서 생성한 캐시 리소스의 [액세스 키(Access keys)] 섹션으로 이동하여, .NET Core Web API에서 사용할 기본 연결 문자열(Primary connection string)을 복사합니다.

단계별 구현
1단계
Visual Studio를 열고 새 .NET Core Web API 프로젝트를 생성합니다.

2단계
새 프로젝트를 구성합니다.
3단계
추가 정보를 입력합니다.
4단계
프로젝트 구조를 확인합니다.
5단계
상품 정보 클래스(ProductDetails)를 생성합니다.
namespace AzureRedisCacheDemo.Models {
public class ProductDetails {
public int Id {
get;
set;
}
public string ProductName {
get;
set;
}
public string ProductDescription {
get;
set;
}
public int ProductPrice {
get;
set;
}
public int ProductStock {
get;
set;
}
}
}
6단계
다음으로 Data 폴더 안에 DbContext 클래스를 추가합니다.
using AzureRedisCacheDemo.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
namespace AzureRedisCacheDemo.Data {
public class DbContextClass: DbContext {
public DbContextClass(DbContextOptions < DbContextClass > options): base(options) {}
public DbSet < ProductDetails > Products {
get;
set;
}
}
}
7단계
이어서 초기 데이터를 삽입하는 데 사용할 SeedData 클래스를 추가합니다.
using AzureRedisCacheDemo.Models;
using Microsoft.EntityFrameworkCore;
namespace AzureRedisCacheDemo.Data
{
public class SeedData
{
public static void Initialize(IServiceProvider serviceProvider)
{
using (var context = new DbContextClass(
serviceProvider.GetRequiredService<DbContextOptions<DbContextClass>>()))
{
if (context.Products.Any())
{
return;
}
context.Products.AddRange(
new ProductDetails
{
Id = 1,
ProductName = "IPhone",
ProductDescription = "IPhone 14",
ProductPrice = 120000,
ProductStock = 100
},
new ProductDetails
{
Id = 2,
ProductName = "Samsung TV",
ProductDescription = "Smart TV",
ProductPrice = 400000,
ProductStock = 120
});
context.SaveChanges();
}
}
}
}
8단계
appsettings.json 파일에 Azure Redis Cache 연결 문자열을 설정합니다.
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"RedisURL": "<valuefromportal>"
}
9단계
Helper 폴더 안에 연결 목적의 ConfigurationManager 클래스와 ConnectionHelper 클래스를 생성합니다.
ConfigurationManager
namespace AzureRedisCacheDemo.Helper {
static class ConfigurationManager {
public static IConfiguration AppSetting {
get;
}
static ConfigurationManager() {
AppSetting = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile("appsettings.json").Build();
}
}
}
ConnectionHelper
using StackExchange.Redis;
namespace AzureRedisCacheDemo.Helper {
public class ConnectionHelper {
static ConnectionHelper() {
ConnectionHelper.lazyConnection = new Lazy < ConnectionMultiplexer > (() => {
return ConnectionMultiplexer.Connect(ConfigurationManager.AppSetting["RedisURL"]);
});
}
private static Lazy < ConnectionMultiplexer > lazyConnection;
public static ConnectionMultiplexer Connection {
get {
return lazyConnection.Value;
}
}
}
}
10단계
다음으로 Repositories 폴더에 IProductService 인터페이스를 추가합니다.
using AzureRedisCacheDemo.Models;
namespace AzureRedisCacheDemo.Repositories {
public interface IProductService {
public Task < List < ProductDetails >> ProductListAsync();
public Task < ProductDetails > GetProductDetailByIdAsync(int productId);
public Task < bool > AddProductAsync(ProductDetails productDetails);
public Task < bool > UpdateProductAsync(ProductDetails productDetails);
public Task < bool > DeleteProductAsync(int productId);
}
}
11단계
이어서 ProductService 클래스를 생성하고 그 안에 IProductService 인터페이스를 구현합니다.
using AzureRedisCacheDemo.Data;
using AzureRedisCacheDemo.Models;
using Microsoft.EntityFrameworkCore;
using System.Linq;
namespace AzureRedisCacheDemo.Repositories {
public class ProductService: IProductService {
private readonly DbContextClass dbContextClass;
public ProductService(DbContextClass dbContextClass) {
this.dbContextClass = dbContextClass;
}
public async Task < List < ProductDetails >> ProductListAsync() {
return await dbContextClass.Products.ToListAsync();
}
public async Task < ProductDetails > GetProductDetailByIdAsync(int productId) {
return await dbContextClass.Products.Where(ele => ele.Id == productId).FirstOrDefaultAsync();
}
public async Task < bool > AddProductAsync(ProductDetails productDetails) {
await dbContextClass.Products.AddAsync(productDetails);
var result = await dbContextClass.SaveChangesAsync();
if (result > 0) {
return true;
} else {
return false;
}
}
public async Task < bool > UpdateProductAsync(ProductDetails productDetails) {
var isProduct = ProductDetailsExists(productDetails.Id);
if (isProduct) {
dbContextClass.Products.Update(productDetails);
var result = await dbContextClass.SaveChangesAsync();
if (result > 0) {
return true;
} else {
return false;
}
}
return false;
}
public async Task < bool > DeleteProductAsync(int productId) {
var findProductData = dbContextClass.Products.Where(_ => _.Id == productId).FirstOrDefault();
if (findProductData != null) {
dbContextClass.Products.Remove(findProductData);
var result = await dbContextClass.SaveChangesAsync();
if (result > 0) {
return true;
} else {
return false;
}
}
return false;
}
private bool ProductDetailsExists(int productId) {
return dbContextClass.Products.Any(e => e.Id == productId);
}
}
}
12단계
IRedisCache 인터페이스를 생성합니다.
namespace AzureRedisCacheDemo.Repositories.AzureRedisCache {
public interface IRedisCache {
T GetCacheData < T > (string key);
bool SetCacheData < T > (string key, T value, DateTimeOffset expirationTime);
object RemoveData(string key);
}
}
13단계
그다음 RedisCache 클래스를 생성하고 앞서 만든 인터페이스의 메서드들을 구현합니다.
using AzureRedisCacheDemo.Helper;
using Newtonsoft.Json;
using StackExchange.Redis;
namespace AzureRedisCacheDemo.Repositories.AzureRedisCache
{
public class RedisCache : IRedisCache
{
private IDatabase _db;
public RedisCache()
{
ConfigureRedis();
}
private void ConfigureRedis()
{
_db = ConnectionHelper.Connection.GetDatabase();
}
public T GetCacheData<T>(string key)
{
var value = _db.StringGet(key);
if (!string.IsNullOrEmpty(value))
{
return JsonConvert.DeserializeObject<T>(value);
}
return default;
}
public object RemoveData(string key)
{
bool _isKeyExist = _db.KeyExists(key);
if (_isKeyExist == true)
{
return _db.KeyDelete(key);
}
return false;
}
public bool SetCacheData<T>(string key, T value, DateTimeOffset expirationTime)
{
TimeSpan expiryTime = expirationTime.DateTime.Subtract(DateTime.Now);
var isSet = _db.StringSet(key, JsonConvert.SerializeObject(value), expiryTime);
return isSet;
}
}
}
14단계
새 Products 컨트롤러를 생성합니다.
using AzureRedisCacheDemo.Models;
using AzureRedisCacheDemo.Repositories;
using AzureRedisCacheDemo.Repositories.AzureRedisCache;
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
namespace AzureRedisCacheDemo.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ProductsController : ControllerBase
{
private readonly IProductService _productService;
private readonly IRedisCache _redisCache;
public ProductsController(IProductService productService, IRedisCache redisCache)
{
_productService = productService;
_redisCache = redisCache;
}
/// <summary>
/// 상품 목록 조회
/// </summary>
/// <returns></returns>
[HttpGet]
public async Task<ActionResult<List<ProductDetails>>> ProductListAsync()
{
var cacheData = _redisCache.GetCacheData<List<ProductDetails>>("product");
if (cacheData != null)
{
return new List<ProductDetails>(cacheData);
}
var productList = await _productService.ProductListAsync();
if(productList != null)
{
var expirationTime = DateTimeOffset.Now.AddMinutes(5.0);
_redisCache.SetCacheData<List<ProductDetails>>("product", productList, expirationTime);
return Ok(productList);
}
else
{
return NoContent();
}
}
/// <summary>
/// ID로 상품 조회
/// </summary>
/// <param name="productId"></param>
/// <returns></returns>
[HttpGet("{productId}")]
public async Task<ActionResult<ProductDetails>> GetProductDetailsByIdAsync(int productId)
{
var cacheData = _redisCache.GetCacheData<List<ProductDetails>>("product");
if (cacheData != null)
{
ProductDetails filteredData = cacheData.Where(x => x.Id == productId).FirstOrDefault();
return new ActionResult<ProductDetails>(filteredData);
}
var productDetails = await _productService.GetProductDetailByIdAsync(productId);
if(productDetails != null)
{
return Ok(productDetails);
}
else
{
return NotFound();
}
}
/// <summary>
/// 신규 상품 추가
/// </summary>
/// <param name="productDetails"></param>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult> AddProductAsync(ProductDetails productDetails)
{
var isProductInserted = await _productService.AddProductAsync(productDetails);
_redisCache.RemoveData("product");
if (isProductInserted)
{
return Ok(isProductInserted);
}
else
{
return BadRequest();
}
}
/// <summary>
/// 상품 정보 수정
/// </summary>
/// <param name="productDetails"></param>
/// <returns></returns>
[HttpPut]
public async Task<IActionResult> UpdateProductAsync(ProductDetails productDetails)
{
var isProductUpdated = await _productService.UpdateProductAsync(productDetails);
_redisCache.RemoveData("product");
if (isProductUpdated)
{
return Ok(isProductUpdated);
}
else
{
return BadRequest();
}
}
/// <summary>
/// ID로 상품 삭제
/// </summary>
/// <param name="productId"></param>
/// <returns></returns>
[HttpDelete]
public async Task<IActionResult> DeleteProductAsync(int productId)
{
var isProductDeleted = await _productService.DeleteProductAsync(productId);
_redisCache.RemoveData("product");
if (isProductDeleted)
{
return Ok(isProductDeleted);
}
else
{
return BadRequest();
}
}
}
}
15단계
Program 클래스에 몇 가지 서비스를 등록합니다.
using AzureRedisCacheDemo.Data;
using AzureRedisCacheDemo.Models;
using AzureRedisCacheDemo.Repositories;
using AzureRedisCacheDemo.Repositories.AzureRedisCache;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
using System;
var builder = WebApplication.CreateBuilder(args);
// 컨테이너에 서비스를 추가합니다.
builder.Services.AddScoped < IProductService, ProductService > ();
builder.Services.AddDbContext < DbContextClass > (o => o.UseInMemoryDatabase("RedisCacheDemo"));
builder.Services.AddScoped < IRedisCache, RedisCache > ();
builder.Services.AddControllers();
// Swagger/OpenAPI 구성에 대한 자세한 내용은 https://aka.ms/aspnetcore/swashbuckle 을 참고하세요.
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
using(var scope = app.Services.CreateScope()) {
var services = scope.ServiceProvider;
var context = services.GetRequiredService < DbContextClass > ();
SeedData.Initialize(services);
}
// HTTP 요청 파이프라인을 구성합니다.
if (app.Environment.IsDevelopment()) {
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
16단계
마지막으로 애플리케이션을 실행하면 Swagger UI에서 API 엔드포인트들을 확인할 수 있습니다.
17단계
상품 조회(GET) 엔드포인트를 호출한 후 Azure Portal에서 Redis CLI를 열어보면, 엔드포인트를 처음 호출했을 때 상품 목록이 캐시에 저장된 것을 확인할 수 있습니다.
이 과정에서는 먼저 해당 데이터가 캐시에 존재하는지 확인합니다. 존재하지 않으면 데이터베이스에서 데이터를 가져온 뒤 캐시에 함께 저장합니다. 이와 관련된 코드는 이미 컨트롤러 안에 작성되어 있으므로, 다음 요청부터는 캐시에서 데이터를 가져오게 됩니다. 컨트롤러 내부에 디버거를 설정해 두면 전체 흐름을 한눈에 파악할 수 있습니다.
GitHub URL
https://github.com/Jaydeep-007/AzureRedisCacheDemo/tree/master/AzureRedisCacheDemo
결론
지금까지 캐시의 기본 개념과 Azure에서의 설정 방법, 그리고 .NET Core Web API를 활용한 단계별 구현 과정을 살펴보았습니다. 이 가이드를 따라 하면 실제 프로젝트에서도 Azure Redis Cache를 손쉽게 적용해 애플리케이션의 성능과 확장성을 크게 개선할 수 있을 것입니다.