13. 캐시(Redis)와 DB 이중 쓰기 일관성 · C#
난이도 상 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 캐시(Redis)와 DB 이중 쓰기 일관성
// ----------------------------------------------------------------------------
// 시나리오 (서버-서버 / 영속화):
// - 캐릭터의 골드는 핫데이터라 Redis 캐시에 두고, 영속 원본은 DB에 둔다.
// - 골드가 바뀌면(거래/보상/구매) UpdateGold(pid, newGold) 가 호출된다.
// 여러 게임 서버 인스턴스/스레드가 같은 캐릭터에 대해 동시에 호출할 수 있다.
// - 읽기는 캐시 우선(read-through): 캐시에 있으면 그 값을, 없으면 DB에서 읽어
// 캐시에 채운 뒤 반환한다.
//
// 요구사항:
// - DB(원본)와 캐시가 영구히 어긋나면 안 된다(특히 부분 실패/동시 갱신 시).
// - 동시 갱신에서 갱신이 유실되면 안 된다(lost update 금지).
// - 캐시는 stale 값으로 고정되면 안 된다(언젠가 원본과 수렴).
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 어떤 인터리빙/실패에서 캐시-DB가 어긋나거나
// 갱신이 유실되는지 설명하고, 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰 후 answer.md 와 대조)
// ============================================================================
using System.Threading.Tasks;
public interface IRedis
{
Task<string> GetAsync(string key);
Task SetAsync(string key, string value);
Task DeleteAsync(string key);
}
public interface IGoldDb
{
Task<long> GetGoldAsync(long playerId);
Task UpdateGoldAsync(long playerId, long newGold);
}
public class PlayerGoldService
{
private readonly IRedis _redis;
private readonly IGoldDb _db;
public PlayerGoldService(IRedis redis, IGoldDb db)
{
_redis = redis;
_db = db;
}
private static string Key(long pid) => $"gold:{pid}";
// 골드 갱신. 여러 서버/스레드에서 동시에 호출될 수 있다.
public async Task UpdateGold(long playerId, long newGold)
{
// (A) 캐시를 먼저 새 값으로 덮어쓴다
await _redis.SetAsync(Key(playerId), newGold.ToString());
// (B) 그 다음 DB(원본)를 갱신한다
await _db.UpdateGoldAsync(playerId, newGold);
}
// 골드 읽기(read-through).
public async Task<long> GetGold(long playerId)
{
string cached = await _redis.GetAsync(Key(playerId));
if (cached != null)
return long.Parse(cached);
// (C) 캐시 미스: DB에서 읽어 캐시에 채운다
long val = await _db.GetGoldAsync(playerId);
await _redis.SetAsync(Key(playerId), val.ToString());
return val;
}
} 결함 코드 · C++
// ============================================================================
// [코드리뷰 문제] C++ - 캐시(Redis)와 DB 이중 쓰기 일관성
// ----------------------------------------------------------------------------
// 시나리오 (서버-서버 / 영속화):
// - 캐릭터의 골드는 핫데이터라 Redis 캐시에 두고, 영속 원본은 DB에 둔다.
// - 골드가 바뀌면(거래/보상/구매) UpdateGold(pid, newGold) 가 호출된다.
// 여러 게임 서버 인스턴스/스레드가 같은 캐릭터에 대해 동시에 호출할 수 있다.
// - 읽기는 캐시 우선(read-through): 캐시에 있으면 그 값을, 없으면 DB에서 읽어
// 캐시에 채운 뒤 반환한다.
//
// 요구사항:
// - DB(원본)와 캐시가 영구히 어긋나면 안 된다(특히 부분 실패/동시 갱신 시).
// - 동시 갱신에서 갱신이 유실되면 안 된다(lost update 금지).
// - 캐시는 stale 값으로 고정되면 안 된다(언젠가 원본과 수렴).
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 어떤 인터리빙/실패에서 캐시-DB가 어긋나거나
// 갱신이 유실되는지 설명하고, 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰 후 answer.md 와 대조)
// ============================================================================
#include <cstdint>
#include <string>
#include <optional>
// 외부 의존(동기 인터페이스로 단순화). 네트워크 호출이라 예외를 던질 수 있다.
struct RedisClient {
std::optional<std::string> Get(const std::string& key);
void Set(const std::string& key, const std::string& value);
void Del(const std::string& key);
};
struct GoldDb {
int64_t GetGold(int64_t playerId);
void UpdateGold(int64_t playerId, int64_t newGold);
};
class PlayerGoldService {
public:
PlayerGoldService(RedisClient& redis, GoldDb& db) : redis_(redis), db_(db) {}
// 골드 갱신. 여러 서버/스레드에서 동시에 호출될 수 있다.
void UpdateGold(int64_t playerId, int64_t newGold)
{
// (A) 캐시를 먼저 새 값으로 덮어쓴다
redis_.Set(Key(playerId), std::to_string(newGold));
// (B) 그 다음 DB(원본)를 갱신한다
db_.UpdateGold(playerId, newGold);
}
// 골드 읽기(read-through).
int64_t GetGold(int64_t playerId)
{
auto cached = redis_.Get(Key(playerId));
if (cached.has_value())
return std::stoll(*cached);
// (C) 캐시 미스: DB에서 읽어 캐시에 채운다
int64_t val = db_.GetGold(playerId);
redis_.Set(Key(playerId), std::to_string(val));
return val;
}
private:
static std::string Key(int64_t pid) { return "gold:" + std::to_string(pid); }
RedisClient& redis_;
GoldDb& db_;
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.