.NET Core와 Redis를 이용한 캐시 관통, 격파, 설사 현상 방지 전략

Redis와 같은 인메모리 데이터베이스를 사용할 때 흔히 발생하는 세 가지 주요 문제인 캐시 관통(Penetration), 캐시 격파(Breakdown), 캐시 설사(Avalanche)는 시스템의 안정성을 위협하는 요소입니다. 이를 효율적으로 제어하기 위한 .NET Core 기반의 해결책을 살펴봅니다.

1. 캐시 문제의 정의 및 대응 방안

  • 캐시 격파 (Cache Breakdown): 특정 'Hot' 데이터가 만료되는 순간 대규모의 동시 요청이 데이터베이스(DB)로 몰리는 현상입니다. 이를 방지하기 위해 데이터 상시 로드(Cache Preheating)를 수행하거나 만료 시간을 설정하지 않는 전략을 사용합니다.
  • 캐시 관통 (Cache Penetration): 캐시와 DB 모두에 존재하지 않는 데이터를 반복적으로 조회하여 리소스를 낭비하는 현상입니다. 악의적인 공격에 취약하며, 이를 막기 위해 블룸 필터(Bloom Filter)를 도입하여 유효하지 않은 키를 사전에 차단합니다.
  • 캐시 설사 (Cache Avalanche): 다량의 캐시 키가 동시에 만료되거나 Redis 서버 자체가 다운되었을 때 모든 트래픽이 DB로 전달되는 현상입니다. 클러스터 구성과 더불어 만료 시간에 난수(Random Jitter)를 추가해 만료 시점을 분산시켜야 합니다.

2. 블룸 필터 및 Redis 환경 설정

.NET Core 환경에서 블룸 필터를 구현하기 위해 BloomFilter.NetCoreCSRedisCore 라이브러리를 사용합니다. 아래는 서비스 등록 예시입니다.

public void ConfigureServices(IServiceCollection services)
{
    // Redis 클라이언트 등록
    var redisClient = new CSRedisClient("192.168.0.192:6379,defaultDatabase=0");
    services.AddSingleton(redisClient);

    // 블룸 필터 설정
    services.AddBloomFilter(config =>
    {
        config.UseCSRedis(new FilterCSRedisOptions
        {
            Name = "AppBloomFilter",
            RedisKey = "filter:global:indices",
            Client = redisClient
        });
    });
}

3. 데이터 생성 및 캐시 무결성 유지

새로운 데이터를 생성할 때 Redis에 저장함과 동시에 블룸 필터에 해당 키를 등록하여 이후 조회 요청 시 유효성을 검증할 수 있도록 합니다.

[HttpPost("order")]
public async Task<IActionResult> CreateOrder([FromBody] OrderRequest request)
{
    string orderId = request.Id.ToString();
    string cacheKey = $"store:order:{orderId}";

    // 1. Redis 캐시 저장 (격파 방지를 위해 만료 시간 미설정 고려)
    await _redisClient.HSetAsync(cacheKey, "data", request);

    // 2. 블룸 필터에 키 추가 (관통 방지)
    await _bloomFilter.AddAsync(cacheKey);

    // 3. 메시지 큐 등을 이용한 후속 처리
    await _capPublisher.PublishAsync("order.created", request);

    return Ok(new { Success = true, Id = orderId });
}

4. 안전한 데이터 조회 로직 구현

조회 시에는 블룸 필터를 거쳐 불필요한 DB 접근을 차단하고, 캐시 누락 시에만 DB를 참조하는 계층적 구조를 가집니다.

[HttpGet("order/{id}")]
public async Task<IActionResult> GetOrderDetails(int id)
{
    string cacheKey = $"store:order:{id}";
    
    // 블룸 필터 검사: 필터에 없는 데이터는 DB에도 없음이 보장됨 (캐시 관통 방지)
    bool isPossibleExist = await _bloomFilter.ContainsAsync(cacheKey);
    if (!isPossibleExist)
    {
        return NotFound("존재하지 않는 요청입니다.");
    }

    // 캐시에서 데이터 조회
    var cachedOrder = await _redisClient.HGetAsync<OrderRequest>(cacheKey, "data");
    
    if (cachedOrder == null)
    {
        // 캐시 격파 상황 대응: 캐시에 없으나 필터는 통과한 경우 DB 조회
        var dbOrder = await _orderService.GetByIdAsync(id);
        
        if (dbOrder == null) return NotFound();

        // 조회된 데이터를 다시 캐싱
        await _redisClient.HSetAsync(cacheKey, "data", dbOrder);
        return Ok(dbOrder);
    }

    return Ok(cachedOrder);
}

이와 같은 설계를 통해 고부하 상황에서도 데이터베이스의 부하를 최소화하고 응답 속도를 일정하게 유지할 수 있습니다. 특히 블룸 필터는 적은 메모리로도 수많은 키의 존재 여부를 효율적으로 판별할 수 있어 대규모 분산 시스템에서 필수적인 도구입니다.

태그: .NET-Core Redis bloom-filter caching-strategy distributed-system

6월 4일 00:29에 게시됨