소개
.NET Core의 캐싱과 작동 방식에 대해 논의하겠습니다. 그래서 다음 사항을 하나씩 살펴보겠습니다.
- 캐싱 소개
- 캐시란 무엇입니까
- 캐시 유형
- 캐시 구현
캐싱은 애플리케이션의 성능과 확장성을 향상시키기 때문에 오늘날 소프트웨어 업계에서 매우 인기가 높습니다. 우리는 Gmail이나 Facebook과 같은 다양한 웹 애플리케이션을 사용하여 반응 속도를 확인하고 있으며 사용자 경험도 훌륭합니다. 인터넷을 사용하는 사용자가 많고 애플리케이션의 네트워크 트래픽과 수요가 막대한 경우 애플리케이션의 성능과 응답성을 개선하는 데 도움이 되는 많은 사항을 처리해야 합니다. 그렇기 때문에 캐싱의 해결책이 있고, 그래서 캐싱이 등장하게 됩니다.
그럼 하나씩 시작해 보겠습니다.
캐싱이란 무엇인가요?
캐시는 자주 액세스하는 데이터를 임시 저장소에 저장하는 데 사용되는 메모리 저장소로, 불필요한 데이터베이스 히트를 방지하고 자주 사용하는 데이터를 필요할 때마다 버퍼에 저장하여 성능을 획기적으로 향상시킵니다.


위 이미지에서 볼 수 있듯이 두 가지 시나리오가 있습니다. 하나는 캐시를 사용하지 않고 다른 하나는 캐시를 사용하는 것입니다. 따라서 여기서 캐시를 사용하지 않는 경우 사용자가 데이터를 원한다고 가정하면 매번 데이터베이스에 도달하게 되며 사용자가 원하는 정적 데이터가 있는 경우 시간 복잡성이 증가하고 성능이 저하되며 이는 모든 사용자에게 동일합니다. 이 경우 캐시를 사용하지 않으면 각각 불필요한 데이터베이스에 접속하여 데이터를 가져옵니다. 반면에 보시다시피 캐시를 사용합니다. 이 경우 모든 사용자에 대해 동일한 정적 및 동일한 데이터가 있는 경우 첫 번째 사용자만 데이터베이스에 접속하여 데이터를 가져와서 캐시 메모리에 저장하고 다른 두 사용자는 데이터를 가져오기 위해 불필요하게 데이터베이스를 사용하지 않고 캐시에서 이를 사용합니다.
캐시 유형
기본적으로 .NET Core가 지원하는 캐싱에는 두 가지 유형이 있습니다.
- 인메모리 캐싱
- 분산 캐싱
In-Memory Cache를 사용하면 데이터가 애플리케이션 서버 메모리에 저장되고 필요할 때마다 여기에서 데이터를 가져와 필요할 때마다 사용합니다. 그리고 분산 캐싱에는 Redis 및 기타 여러 타사 메커니즘이 많이 있습니다. 하지만 이 섹션에서는 Redis Cache를 자세히 살펴보고 .NET Core에서 작동하는 방식을 살펴보겠습니다.
분산 캐싱

- 기본적으로 분산 캐싱에서는 데이터가 여러 서버에 저장되고 공유됩니다.
- 또한 멀티 테넌트 애플리케이션을 사용하면 여러 서버 간의 부하를 관리한 후 애플리케이션의 확장성과 성능을 쉽게 향상시킬 수 있습니다.
- 미래에 하나의 서버가 충돌하고 다시 시작되면 우리가 원하는 경우 여러 서버가 우리의 필요에 따라 이루어지기 때문에 애플리케이션이 아무런 영향을 미치지 않는다고 가정합니다.
Redis는 현재 많은 회사에서 애플리케이션의 성능과 확장성을 개선하기 위해 사용하는 가장 인기 있는 캐시입니다. 그래서 Redis와 그 사용법을 하나씩 살펴보도록 하겠습니다.
레디스 캐시
- Redis는 데이터베이스로 사용되는 오픈 소스(BSD 라이선스) 인메모리 데이터 구조 저장소입니다.
- 기본적으로 자주 사용되는 데이터와 일부 정적 데이터를 캐시 내부에 저장하고 사용자 요구 사항에 따라 사용하고 예약하는 데 사용됩니다.
- Redis에는 List, Set, Hashing, Stream 등과 같이 데이터를 저장하는 데 사용할 수 있는 많은 데이터 구조가 있습니다.
Redis Cache 설치
1단계. 다음 URL을 사용하여 Redis 서버를 다운로드하세요.
https://github.com/microsoftarchive/redis/releases/tag/win-3.0.504
2단계. zip 파일의 압축을 풀고 나중에 Redis 서버와 Redis CLI를 엽니다.

.NET Core API를 사용한 Redis Cache 구현
1단계. .NET Core API 웹 애플리케이션 생성
2단계. 애플리케이션에서 단계별로 필요한 다음 NuGet 패키지를 설치하세요.
- Microsoft.EntityFrameworkCore
- Microsoft.EntityFrameworkCore.Design
- Microsoft.EntityFrameworkCore.SqlServer
- Microsoft.EntityFrameworkCore.Tools
- Swashbuckle.AspNetCore
- StackExchange.Redis
3단계. 모델 폴더를 생성하고 그 안에 세부정보가 포함된 하나의 제품 클래스를 생성하세요.
namespace RedisCacheDemo.Model
{
public class Product
{
public int ProductId { get; set; }
public string ProductName { get; set; }
public string ProductDescription { get; set; }
public int Stock { get; set; }
}
}
4단계. 다음으로, 아래 표시된 대로 데이터베이스 작업을 위한 DbContextClass 클래스를 만듭니다.
using Microsoft.EntityFrameworkCore;
using RedisCacheDemo.Model;
namespace RedisCacheDemo.Data {
public class DbContextClass: DbContext {
public DbContextClass(DbContextOptions < DbContextClass > options): base(options) {}
public DbSet < Product > Products {
get;
set;
}
}
} 5단계. 이제 Redis Cache 관련 사용을 위한 ICacheService 인터페이스와 CacheService 클래스를 생성하겠습니다.
using System;
namespace RedisCacheDemo.Cache
{
public interface ICacheService
{
/// <summary>
/// Get Data using key
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key"></param>
/// <returns></returns>
T GetData<T>(string key);
/// <summary>
/// Set Data with Value and Expiration Time of Key
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="key"></param>
/// <param name="value"></param>
/// <param name="expirationTime"></param>
/// <returns></returns>
bool SetData<T>(string key, T value, DateTimeOffset expirationTime);
/// <summary>
/// Remove Data
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
object RemoveData(string key);
}
}
using Newtonsoft.Json;
using StackExchange.Redis;
using System;
namespace RedisCacheDemo.Cache {
public class CacheService: ICacheService {
private IDatabase _db;
public CacheService() {
ConfigureRedis();
}
private void ConfigureRedis() {
_db = ConnectionHelper.Connection.GetDatabase();
}
public T GetData < T > (string key) {
var value = _db.StringGet(key);
if (!string.IsNullOrEmpty(value)) {
return JsonConvert.DeserializeObject < T > (value);
}
return default;
}
public bool SetData < 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;
}
public object RemoveData(string key) {
bool _isKeyExist = _db.KeyExists(key);
if (_isKeyExist == true) {
return _db.KeyDelete(key);
}
return false;
}
}
} 6단계. ProductController 클래스를 생성하고 아래와 같이 다음 메소드를 생성합니다.
using Microsoft.AspNetCore.Mvc;
using RedisCacheDemo.Cache;
using RedisCacheDemo.Data;
using RedisCacheDemo.Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace RedisCacheDemo.Controllers {
[Route("api/[controller]")]
[ApiController]
public class ProductController: ControllerBase {
private readonly DbContextClass _dbContext;
private readonly ICacheService _cacheService;
public ProductController(DbContextClass dbContext, ICacheService cacheService) {
_dbContext = dbContext;
_cacheService = cacheService;
}
[HttpGet("products")]
public IEnumerable < Product > Get() {
var cacheData = _cacheService.GetData < IEnumerable < Product >> ("product");
if (cacheData != null) {
return cacheData;
}
var expirationTime = DateTimeOffset.Now.AddMinutes(5.0);
cacheData = _dbContext.Products.ToList();
_cacheService.SetData < IEnumerable < Product >> ("product", cacheData, expirationTime);
return cacheData;
}
[HttpGet("product")]
public Product Get(int id) {
Product filteredData;
var cacheData = _cacheService.GetData < IEnumerable < Product >> ("product");
if (cacheData != null) {
filteredData = cacheData.Where(x => x.ProductId == id).FirstOrDefault();
return filteredData;
}
filteredData = _dbContext.Products.Where(x => x.ProductId == id).FirstOrDefault();
return filteredData;
}
[HttpPost("addproduct")]
public async Task < Product > Post(Product value) {
var obj = await _dbContext.Products.AddAsync(value);
_cacheService.RemoveData("product");
_dbContext.SaveChanges();
return obj.Entity;
}
[HttpPut("updateproduct")]
public void Put(Product product) {
_dbContext.Products.Update(product);
_cacheService.RemoveData("product");
_dbContext.SaveChanges();
}
[HttpDelete("deleteproduct")]
public void Delete(int Id) {
var filteredData = _dbContext.Products.Where(x => x.ProductId == Id).FirstOrDefault();
_dbContext.Remove(filteredData);
_cacheService.RemoveData("product");
_dbContext.SaveChanges();
}
}
} 7단계. appsetting.json 내에 SQL Server 연결 문자열과 Redis URL을 추가합니다.
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"RedisURL": "127.0.0.1:6379",
"ConnectionStrings": {
"DefaultConnection": "Data Source=Server;Initial Catalog=RedisCache;User Id=sa;Password=***;"
}
} 8단계. 다음으로, Startup Class의 Configure Service 메소드 내에 ICacheService를 등록하고 Swagger와 관련된 일부 구성을 추가하여 API 엔드포인트를 테스트하세요.
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.OpenApi.Models;
using RedisCacheDemo.Cache;
using RedisCacheDemo.Data;
namespace RedisCacheDemo {
public class Startup {
public Startup(IConfiguration configuration) {
Configuration = configuration;
}
public IConfiguration Configuration {
get;
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services) {
services.AddControllers();
services.AddScoped < ICacheService, CacheService > ();
services.AddDbContext < DbContextClass > (options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddSwaggerGen(c => {
c.SwaggerDoc("v1", new OpenApiInfo {
Title = "RedisCacheDemo", Version = "v1"
});
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) {
if (env.IsDevelopment()) {
app.UseDeveloperExceptionPage();
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "RedisCacheDemo v1"));
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints => {
endpoints.MapControllers();
});
}
}
} 9단계. 하나의 ConfigurationManger 클래스를 생성하여 거기에서 앱 설정을 구성하세요
using Microsoft.Extensions.Configuration;
using System.IO;
namespace RedisCacheDemo {
static class ConfigurationManager {
public static IConfiguration AppSetting {
get;
}
static ConfigurationManager() {
AppSetting = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile("appsettings.json").Build();
}
}
} 10단계. 다음으로 Redis 연결을 위한 연결 도우미 클래스를 만듭니다.
using StackExchange.Redis;
using System;
namespace RedisCacheDemo.Cache {
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;
}
}
}
} 11단계. 패키지 관리자 콘솔에서 다음 명령을 사용하여 DB 생성을 위한 마이그레이션 및 데이터베이스 업데이트를 수행합니다.
add-migration “FirstMigration”
update-database 따라서 이 명령을 입력하고 실행하면 appsetting.json의 연결 문자열에 입력한 대로 마이그레이션과 관련된 몇 가지 항목이 생성되고 SQL Server 내부에 데이터베이스가 생성됩니다.
12단계. 마지막으로 애플리케이션을 실행하고 Swagger UI를 사용하여 데이터를 추가한 다음 제품 및 제품 엔드포인트 내에서 캐싱이 어떻게 작동하는지 확인하세요.
기본적으로 컨트롤러의 제품 및 제품 엔드포인트에 캐시를 추가했습니다. 보시다시피, 사용자가 모든 제품의 데이터를 가져오려고 하면 먼저 Redis Cache 내부에 데이터가 있는지 확인하고, 캐시 안에 있으면 해당 데이터를 사용자에게 반환하고, 캐시 내부에 데이터가 없으면 데이터베이스에서 데이터를 가져와서 캐시에 설정합니다. 따라서 다음 번에는 사용자가 캐시에서만 해당 정보를 가져오고 불필요하게 데이터베이스에 충돌하는 것을 방지할 수 있습니다.
또한 사용자가 제품 ID를 사용하여 데이터를 가져오려는 경우 제품 두 번째 엔드포인트의 컨트롤러에서 볼 수 있듯이 모든 제품의 캐시에서 데이터를 가져온 다음 제품 ID를 사용하여 필터링합니다. 존재하는 경우 캐시에서 사용자에게 반환됩니다. 그렇지 않은 경우 데이터베이스에서 가져오고 필터를 적용한 후 사용자에게 반환됩니다.
제품 컨트롤러의 업데이트, 삭제 및 게시 끝점 내부에서 볼 수 있듯이 제거 메서드를 사용하여 캐시에서 제품 키 데이터를 제거합니다. 필요와 요구 사항에 따라 사용할 수 있는 메모리 캐시에는 다양한 시나리오와 용도가 있습니다. Redis Cache의 기본 사항과 여기에서 다룬 .NET Core 내에서 작동하는 방식을 소개하고 싶습니다.
또한 캐싱을 사용하는 동안 주의해야 할 시나리오가 하나 있습니다. 두 명의 사용자가 귀하의 애플리케이션을 사용하고 있다고 가정합니다. 그러면 다음과 같은 시나리오가 발생합니다.
- 첫 번째 사용자가 모든 상품의 데이터를 가져오는 요청을 보내면 첫 번째 요청이 들어오고 그 후 캐시 내부에 데이터가 있는지 확인합니다. 데이터가 캐시 내부에 있으면 데이터베이스에서 데이터를 가져와서 캐시에 설정합니다.
- 그동안 두 번째 사용자는 제품 세부정보를 가져오기 위해 요청을 보냅니다. 첫 번째 사용자의 요청이 완료되기 전에 해당 요청도 데이터베이스에 도달했고, 이로 인해 두 번째 사용자도 제품 세부정보를 가져오기 위해 데이터베이스에 도달했습니다.
- 따라서 아래와 같이 잠금 메커니즘을 사용하는 한 가지 해결책이 있습니다
클래스 위에 이 비공개 잠금 개체를 만듭니다.
private static object _lock = new object() 다음으로, 아래 표시된 대로 Get 메서드를 수정합니다.
public IEnumerable < Product > Get() {
var cacheData = _cacheService.GetData < IEnumerable < Product >> ("product");
if (cacheData != null) {
return cacheData;
}
lock(_lock) {
var expirationTime = DateTimeOffset.Now.AddMinutes(5.0);
cacheData = _dbContext.Products.ToList();
_cacheService.SetData < IEnumerable < Product >> ("product", cacheData, expirationTime);
}
return cacheData;
} 따라서 여기서는 보시다시피 먼저 데이터가 캐시 내부에 존재하는지 확인합니다. 데이터를 사용할 수 있으면 해당 데이터를 반환하세요. 다음으로 Redis 캐시에 값이 없으면 거기에 잠금을 적용한 다음 요청이 잠기고 데이터베이스에서 제품 세부 정보를 가져온 다음 캐시에 설정하고 데이터를 반환하는 섹션에 입력됩니다. 그렇다면 사용자의 요청이 완료되기 전에 두 번째 사용자가 요청을 보내면 어떻게 될까요? 따라서 이 경우 두 번째 요청이 대기열에 있고 첫 번째 사용자 요청이 완료된 후 두 번째 요청이 등장합니다
또한 아래와 같이 Redis CLI를 사용하여 Redis 내부에 이미 존재하는 주요 세부 정보를 확인할 수 있습니다.

따라서 여기서는 Redis Cache에 존재하는 키에 대한 정보를 제공하는 많은 명령이 있음을 볼 수 있습니다.
이것은 .NET Core의 Redis Cache에 관한 것입니다. 이에 관련된 내용을 이해해주시기 바랍니다.
즐거운 코딩 되세요!