4. 읽기-통과 캐시의 캐시 스탬피드(thundering herd)
난이도 상 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 읽기-통과(read-through) 캐시의 TTL 만료와 동시 미스
// ----------------------------------------------------------------------------
// 시나리오 (영속화/캐시 일관성):
// - 길드 랭킹은 계산이 비싸(여러 테이블 집계) 결과를 분산 캐시(Redis)에 TTL 30초로 둔다.
// - 요청이 오면 캐시를 보고, 없으면 DB 에서 계산해 캐시에 채운 뒤 돌려준다.
// - 인기 길드의 랭킹 키는 초당 수천 건씩 조회된다.
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 인기 키의 TTL 이 만료되는 순간 동시에 수천 요청이
// 들어올 때 DB 와 응답 지연이 어떻게 되는지 설명하라. 또 존재하지 않는 키가 반복 조회될
// 때의 동작도 논하라. 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
using System;
using System.Threading.Tasks;
public class Ranking { /* ... 집계 결과 ... */ }
public interface IDistributedCache
{
Task<byte[]> GetAsync(string key);
Task SetAsync(string key, byte[] value, TimeSpan ttl);
}
public interface IRankingDb
{
Task<Ranking> ComputeRankingAsync(string key); // 비싼 집계 쿼리
}
public class RankingCache
{
private static readonly TimeSpan Ttl = TimeSpan.FromSeconds(30);
private readonly IDistributedCache _cache;
private readonly IRankingDb _db;
public RankingCache(IDistributedCache cache, IRankingDb db) { _cache = cache; _db = db; }
public async Task<Ranking> GetAsync(string key)
{
var cached = await _cache.GetAsync(key); // (A) 캐시 조회
if (cached != null)
return Deserialize(cached);
var data = await _db.ComputeRankingAsync(key); // (B) 비싼 재계산(DB)
await _cache.SetAsync(key, Serialize(data), Ttl); // (C) 캐시 채움 (고정 TTL)
return data;
}
private static byte[] Serialize(Ranking r) => Array.Empty<byte>();
private static Ranking Deserialize(byte[] b) => new Ranking();
} 결함 코드 · C++
// ============================================================================
// [코드리뷰 문제] C++ - 읽기-통과(read-through) 캐시의 TTL 만료와 동시 미스
// ----------------------------------------------------------------------------
// 시나리오 (영속화/캐시 일관성):
// - 길드 랭킹은 계산이 비싸(여러 테이블 집계) 결과를 프로세스 내 캐시에 TTL 30초로 둔다.
// - 요청이 오면 캐시를 보고, 없으면 DB 에서 계산해 캐시에 채운 뒤 돌려준다.
// - 인기 키는 여러 워커 스레드에서 초당 수천 건씩 조회된다.
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 인기 키의 TTL 이 만료되는 순간 동시에 수천 요청이
// 들어올 때 DB 와 응답 지연이 어떻게 되는지 설명하라. 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
#include <string>
#include <unordered_map>
#include <mutex>
#include <shared_mutex>
#include <chrono>
struct Ranking { /* ... 집계 결과 ... */ };
class IRankingDb {
public:
virtual Ranking computeRanking(const std::string& key) = 0; // 비싼 집계 쿼리
virtual ~IRankingDb() = default;
};
class RankingCache {
public:
explicit RankingCache(IRankingDb& db) : db_(db) {}
Ranking get(const std::string& key) {
using clock = std::chrono::steady_clock;
{
std::shared_lock<std::shared_mutex> rl(mtx_); // (A) 읽기 락으로 조회
auto it = cache_.find(key);
if (it != cache_.end() && clock::now() < it->second.expireAt)
return it->second.value;
} // 여기서 읽기 락 해제
Ranking data = db_.computeRanking(key); // (B) 비싼 재계산(DB)
{
std::unique_lock<std::shared_mutex> wl(mtx_); // (C) 쓰기 락으로 채움
cache_[key] = Entry{ data, clock::now() + std::chrono::seconds(30) };
}
return data;
}
private:
struct Entry {
Ranking value;
std::chrono::steady_clock::time_point expireAt;
};
IRankingDb& db_;
std::shared_mutex mtx_;
std::unordered_map<std::string, Entry> cache_;
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.