19. 해설 (C#) — 비동기 영속화 큐가 밀릴 때 최신값 유실/순서 역전

난이도 최상 해설 보기 →

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

결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 비동기 영속화 큐가 밀릴 때 최신값 유실/순서 역전
// ----------------------------------------------------------------------------
// 시나리오 (영속화 / 동시성):
//   - 게임 로직은 캐릭터 상태(골드/레벨 등)를 메모리에서 실시간으로 바꾸고, 변경이
//     생기면 MarkDirty(charId, state) 로 "저장해 달라" 고 영속화 큐에 알린다.
//   - 같은 캐릭터의 연속 변경을 매번 DB 에 쓰면 비싸므로, 캐릭터별 "최신 스냅샷" 만
//     보관(coalesce)하고, 더티 캐릭터 ID 를 큐에 한 번만 넣는다.
//   - 여러 "저장 워커" 스레드가 큐에서 ID 를 꺼내 latest 스냅샷을 읽어 DB 에 쓴다.
//   - 부하가 높으면 DB 쓰기가 느려져 큐가 밀리고, 같은 캐릭터가 짧은 간격으로 여러 번
//     더티가 된다(거래·전투 보상 연타 등).
//
// 요구사항:
//   - 한 캐릭터에 대해 "더 최근 상태가 더 오래된 상태로 덮여 저장" 되면 안 된다(순서 역전 금지).
//   - 더티가 된 변경분은 반드시 언젠가 DB 에 반영돼야 한다(최신값 유실 금지).
//   - 동시에 여러 워커가 돌아도 큐/맵 자료구조가 손상되거나 예외로 죽으면 안 된다.
//
// 과제:
//   이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 순서 역전·유실·손상이 나는지(인터리빙
//   포함) 설명하고, 수정안과 더 나은 설계를 제시하라.
//   (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================

using System;
using System.Collections.Generic;

public struct CharState
{
    public long Gold;
    public int  Level;
}

public class PersistenceQueue
{
    private readonly Dictionary<long, CharState> _latest  = new();   // 캐릭터별 최신 스냅샷
    private readonly HashSet<long>                _inQueue = new();   // 현재 큐에 들어있는 ID
    private readonly Queue<long>                  _dirty   = new();   // 더티 ID 큐

    // 게임 로직 스레드들이 호출(여러 스레드에서 동시 호출 가능)
    public void MarkDirty(long charId, CharState s)
    {
        _latest[charId] = s;                 // 최신 스냅샷으로 덮어쓰기(coalesce)
        if (_inQueue.Add(charId))            // 아직 큐에 없으면
            _dirty.Enqueue(charId);          // 큐에 한 번만 넣기
    }

    // 저장 워커 스레드들이 반복 호출
    public void WorkerStep()
    {
        if (_dirty.Count == 0) return;

        long id = _dirty.Dequeue();
        _inQueue.Remove(id);          // (A) 큐에서 꺼내며 더티 표시 해제

        CharState snap = _latest[id]; // (B) 현재 최신 스냅샷을 읽어
        WriteToDb(id, snap);          // (C) DB 에 비동기로 기록(느릴 수 있음)
    }

    private void WriteToDb(long id, CharState s)
    {
        // 실제 DB 비동기 쓰기(네트워크 왕복, 수십~수백 ms 소요 가능)
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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