카테고리 없음

쿼리캐싱

keisoft 2025. 5. 8. 13:59

안녕하세요! C# Blazor에서 딕셔너리에 쿼리 키와 쿼리 결과를 저장하고, 특정 시간이 지나면 자동으로 삭제되게 하는 방법을 알려드리겠습니다.

이런 요구사항을 구현하는 좋은 방법은 `MemoryCache` 또는 커스텀 캐시 솔루션을 사용하는 것입니다. 아래 두 가지 접근법을 설명해 드리겠습니다.

## 1. MemoryCache 사용하기

`Microsoft.Extensions.Caching.Memory` 패키지를 사용하면 쉽게 구현할 수 있습니다:

```csharp
@page "/"
@using Microsoft.Extensions.Caching.Memory
@inject IMemoryCache Cache

@code {
    private void StoreQueryResult(string queryKey, object queryResult)
    {
        // 만료 시간 설정 (예: 10분 후 만료)
        var cacheEntryOptions = new MemoryCacheEntryOptions()
            .SetAbsoluteExpiration(TimeSpan.FromMinutes(10));

        // 캐시에 결과 저장
        Cache.Set(queryKey, queryResult, cacheEntryOptions);
    }

    private object GetQueryResult(string queryKey)
    {
        // 캐시에서 결과 가져오기 (없으면 null 반환)
        Cache.TryGetValue(queryKey, out object result);
        return result;
    }
}
```

## 2. 커스텀 캐시 서비스 구현하기

더 많은 제어가 필요한 경우, 자체 캐시 서비스를 구현할 수 있습니다:

using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;

namespace YourNamespace.Services
{
    public class CacheItem<T>
    {
        public T Value { get; set; }
        public DateTime ExpiryTime { get; set; }
    }

    public interface ITimeoutCacheService
    {
        void Set<T>(string key, T value, TimeSpan expiryTime);
        bool TryGetValue<T>(string key, out T value);
        void Remove(string key);
    }

    public class TimeoutCacheService : BackgroundService, ITimeoutCacheService
    {
        private readonly ConcurrentDictionary<string, object> _cache = new ConcurrentDictionary<string, object>();
        private readonly TimeSpan _cleanupInterval;

        public TimeoutCacheService(TimeSpan? cleanupInterval = null)
        {
            _cleanupInterval = cleanupInterval ?? TimeSpan.FromMinutes(1);
        }

        public void Set<T>(string key, T value, TimeSpan expiryTime)
        {
            var expiryDateTime = DateTime.UtcNow.Add(expiryTime);
            var cacheItem = new CacheItem<T>
            {
                Value = value,
                ExpiryTime = expiryDateTime
            };

            _cache[key] = cacheItem;
        }

        public bool TryGetValue<T>(string key, out T value)
        {
            value = default;

            if (_cache.TryGetValue(key, out var item) && item is CacheItem<T> typedItem)
            {
                if (typedItem.ExpiryTime > DateTime.UtcNow)
                {
                    value = typedItem.Value;
                    return true;
                }
                else
                {
                    // 이미 만료된 아이템이면 삭제
                    _cache.TryRemove(key, out _);
                }
            }

            return false;
        }

        public void Remove(string key)
        {
            _cache.TryRemove(key, out _);
        }

        protected override async Task ExecuteAsync(CancellationToken stoppingToken)
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                CleanupExpiredItems();
                await Task.Delay(_cleanupInterval, stoppingToken);
            }
        }

        private void CleanupExpiredItems()
        {
            var now = DateTime.UtcNow;
            
            foreach (var key in _cache.Keys)
            {
                if (_cache.TryGetValue(key, out var item))
                {
                    // 모든 타입의 CacheItem에서 ExpiryTime 속성에 접근
                    var expiryTime = item.GetType().GetProperty("ExpiryTime")?.GetValue(item) as DateTime?;
                    
                    if (expiryTime.HasValue && expiryTime.Value <= now)
                    {
                        _cache.TryRemove(key, out _);
                    }
                }
            }
        }
    }
}

이제 이 서비스를 Blazor 애플리케이션에 등록하고 사용하는 방법을 설명해 드리겠습니다:

// Program.cs에 서비스 등록
builder.Services.AddSingleton<ITimeoutCacheService, TimeoutCacheService>();
builder.Services.AddHostedService(provider => provider.GetRequiredService<TimeoutCacheService>());

// Blazor 컴포넌트에서 사용
@page "/cache-example"
@using YourNamespace.Services
@inject ITimeoutCacheService CacheService

<h3>캐시 예제</h3>

<button @onclick="StoreData">데이터 저장</button>
<button @onclick="RetrieveData">데이터 검색</button>

<p>결과: @result</p>

@code {
    private string result = "";

    private void StoreData()
    {
        // 쿼리 키와 결과 저장 (예: 5분 후 만료)
        string queryKey = "example-query";
        string queryResult = "쿼리 결과 데이터 " + DateTime.Now.ToString();
        
        CacheService.Set(queryKey, queryResult, TimeSpan.FromMinutes(5));
        result = $"저장됨: {queryResult} (5분 후 만료)";
    }

    private void RetrieveData()
    {
        string queryKey = "example-query";
        
        if (CacheService.TryGetValue(queryKey, out string value))
        {
            result = $"검색됨: {value}";
        }
        else
        {
            result = "데이터가 없거나 만료되었습니다.";
        }
    }
}

## 3. 사용 시 고려 사항

1. **메모리 관리**: 캐시는 메모리를 사용하므로, 너무 많은 데이터를 캐시하면 메모리 사용량이 증가할 수 있습니다. 필요한 데이터만 적절한 시간 동안 캐시하세요.

2. **만료 시간 설정**: 데이터의 중요도와 갱신 빈도에 따라 적절한 만료 시간을 설정하세요.
   - 자주 변경되는 데이터: 짧은 만료 시간 (몇 분)
   - 비교적 정적인 데이터: 긴 만료 시간 (몇 시간)

3. **서버 측 Blazor vs 클라이언트 측 Blazor**:
   - 서버 측 Blazor에서는 모든 사용자의 캐시가 서버에 저장됩니다.
   - 클라이언트 측 Blazor WebAssembly에서는 각 사용자가 자신의 브라우저에 캐시를 저장합니다.

4. **분산 환경**: 여러 서버를 사용하는 경우, Redis와 같은 분산 캐시 솔루션을 고려하세요.

## 더 간단한 접근법: ConcurrentDictionary + Timer

아래는 더 간단한 방식으로 ConcurrentDictionary와 Timer를 사용한 예제입니다:

using System;
using System.Collections.Concurrent;
using System.Threading;

public class SimpleTimeoutCache<TKey, TValue>
{
    private class CacheEntry
    {
        public TValue Value { get; set; }
        public DateTime ExpiryTime { get; set; }
    }

    private readonly ConcurrentDictionary<TKey, CacheEntry> _cache = new ConcurrentDictionary<TKey, CacheEntry>();
    private readonly Timer _cleanupTimer;

    public SimpleTimeoutCache(TimeSpan cleanupInterval)
    {
        // 주기적으로 만료된 항목을 제거하는 타이머 설정
        _cleanupTimer = new Timer(CleanupCallback, null, cleanupInterval, cleanupInterval);
    }

    public void Set(TKey key, TValue value, TimeSpan expiryTime)
    {
        var entry = new CacheEntry
        {
            Value = value,
            ExpiryTime = DateTime.UtcNow.Add(expiryTime)
        };

        _cache[key] = entry;
    }

    public bool TryGetValue(TKey key, out TValue value)
    {
        value = default;

        if (_cache.TryGetValue(key, out var entry))
        {
            if (entry.ExpiryTime > DateTime.UtcNow)
            {
                value = entry.Value;
                return true;
            }
            else
            {
                // 이미 만료된 항목 제거
                _cache.TryRemove(key, out _);
            }
        }

        return false;
    }

    public void Remove(TKey key)
    {
        _cache.TryRemove(key, out _);
    }

    private void CleanupCallback(object state)
    {
        var now = DateTime.UtcNow;

        foreach (var key in _cache.Keys)
        {
            if (_cache.TryGetValue(key, out var entry) && entry.ExpiryTime <= now)
            {
                _cache.TryRemove(key, out _);
            }
        }
    }

    // 리소스 해제
    public void Dispose()
    {
        _cleanupTimer?.Dispose();
    }
}

// 사용 예제
public class QueryService
{
    private readonly SimpleTimeoutCache<string, object> _queryCache =
        new SimpleTimeoutCache<string, object>(TimeSpan.FromMinutes(5));

    public object GetQueryResult(string queryKey)
    {
        if (_queryCache.TryGetValue(queryKey, out var result))
        {
            return result;
        }

        // 쿼리 실행 로직
        var queryResult = ExecuteQuery(queryKey);
        
        // 캐시에 결과 저장 (30분 동안 유효)
        _queryCache.Set(queryKey, queryResult, TimeSpan.FromMinutes(30));
        
        return queryResult;
    }

    private object ExecuteQuery(string queryKey)
    {
        // 실제 쿼리 실행 로직
        return new { Data = "쿼리 결과", Timestamp = DateTime.Now };
    }
}

## 요약

Blazor에서 시간 기반 캐시를 구현하는 방법은 다음과 같습니다:

1. **기본 MemoryCache 사용**: 가장 간단한 방법으로, Microsoft.Extensions.Caching.Memory 패키지를 사용합니다.

2. **커스텀 캐시 서비스**: 더 많은 제어가 필요한 경우 BackgroundService를 확장하여 구현할 수 있습니다.

3. **간단한 ConcurrentDictionary + Timer**: 단순한 시나리오에서는 ConcurrentDictionary와 Timer를 조합하여 사용할 수 있습니다.

선택하신 방법이 애플리케이션의 요구 사항에 맞게 쿼리 결과를 캐시하고 특정 시간이 지나면 자동으로 삭제되도록 도움을 줄 것입니다. 구현 시 추가 도움이 필요하시다면 말씀해 주세요!