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#
내 답안 · 자동 저장

작성 후 위 해설 보기에서 모범 해설과 대조하세요.