23. 실시간 랭킹 캐시와 원본 DB의 일관성 (C#)

난이도 최상 해설 보기 →

결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커 (A)(B) 는 주목 위치 힌트다.

결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 실시간 랭킹 캐시와 원본 DB의 일관성
// ----------------------------------------------------------------------------
// 시나리오 (동시성/메모리 · 서버-서버/서버-클라):
//   - 점수가 바뀔 때마다 권위 저장소(DB)에 기록하고, 실시간 조회를 빠르게 하려고
//     인메모리 정렬 캐시(랭킹)를 함께 유지한다(write-through).
//   - 캐시는 두 구조로 구성: _scoreOf(playerId -> 현재 점수)와
//     _ranking(정렬된 (점수, playerId) 집합; 상위 N 조회용).
//   - UpdateScore 는 여러 워커 스레드에서 같은/다른 플레이어에 대해 동시에 불린다.
//   - 캐시 미스(서버 재기동/축출)면 RebuildFromDb 로 DB를 읽어 캐시를 재구축한다.
//
// 요구사항:
//   - 캐시의 점수/랭킹은 DB와 수렴해야 한다(영구 불일치 금지).
//   - 같은 플레이어에 대한 동시/순서 뒤바뀐 갱신에도 캐시에 "더 낮은(옛) 점수" 가
//     최종으로 남으면 안 된다(out-of-order 역전 금지).
//   - 랭킹 집합에서 한 플레이어는 항상 정확히 한 엔트리(중복/소실 금지).
//   - 재구축이 동시 갱신을 덮어써 유실시키면 안 된다.
//
// 과제:
//   이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 캐시-DB 불일치·랭킹 중복/소실·순서 역전·
//   재구축 중 유실·동시 컬렉션 손상이 동시 인터리빙으로 발생하는지 설명하고, 수정안과 더 나은
//   설계를 제시하라. (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================

using System;
using System.Collections.Generic;

public interface IScoreDb
{
    void WriteScore(long playerId, long score);
    List<(long playerId, long score)> LoadAll();
}

public class RankingCache
{
    private readonly IScoreDb _db;
    private readonly Dictionary<long, long> _scoreOf = new();              // playerId -> 점수
    private readonly SortedSet<(long score, long playerId)> _ranking = new(); // (점수, id) 오름차순

    public RankingCache(IScoreDb db) { _db = db; }

    // 점수 갱신: DB 기록 후 캐시 갱신 (여러 스레드 동시 호출)
    public void UpdateScore(long playerId, long newScore)
    {
        // (A) DB 기록과 캐시 갱신의 순서/관계를 검토하라
        _db.WriteScore(playerId, newScore);

        // (B) 옛 엔트리 제거 → 새 엔트리 삽입 절차를 검토하라
        if (_scoreOf.TryGetValue(playerId, out var old))
        {
            _ranking.Remove((old, playerId));
            _scoreOf[playerId] = newScore;
        }
        else
        {
            _scoreOf[playerId] = newScore;
        }
        _ranking.Add((newScore, playerId));
    }

    // 상위 N 조회 (조회 스레드)
    public List<long> TopN(int n)
    {
        var outList = new List<long>();
        // (C) 이 순회를 동시 갱신과 함께 검토하라
        foreach (var e in _ranking) // 오름차순; 실제로는 역순(상위)부터 필요하나 데모상 단순화
        {
            outList.Add(e.playerId);
            if (outList.Count >= n) break;
        }
        return outList;
    }

    // 캐시 미스 시 DB에서 전체 재구축
    public void RebuildFromDb()
    {
        // (D) 이 재구축 절차를 동시 갱신과 함께 검토하라
        var all = _db.LoadAll();
        _scoreOf.Clear();
        _ranking.Clear();
        foreach (var (pid, sc) in all)
        {
            _scoreOf[pid] = sc;
            _ranking.Add((sc, pid));
        }
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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