11. 주기적 스냅샷 저장 vs 즉시 변경 (C#)

난이도 중 해설 보기 →

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

결함 코드 · C#
// ============================================================================
// 시나리오
// ----------------------------------------------------------------------------
// 게임서버의 주기적 영속화(스냅샷 저장)다. 캐릭터 상태(골드/레벨/경험치 등)는
// 메모리에 두고 게임 로직이 실시간으로 바꾼다. 데이터 유실을 줄이려고 전용
// "스냅샷 스레드" 가 N초마다 변경된(dirty) 캐릭터를 훑어 현재 상태를 떠서
// 저장 큐에 넣고, 여러 "저장 워커" 가 큐에서 꺼내 DB 에 비동기로 기록한다.
//  - 게임 워커 스레드들은 동시에 같은 캐릭터의 상태를 변경한다(골드 가산 등).
//  - "스냅샷 스레드는 읽기만 하니 락이 필요 없다"고 가정했다.
//  - "한 캐릭터는 한 번에 하나만 저장되겠지"라고 가정했다.
//
// 운영 중 증상(드물고 비결정적, 부하 의존):
//  - 가끔 재기동 후 캐릭터 상태가 "과거 값"으로 되돌아간다(최근 변경 유실).
//  - 가끔 골드와 레벨/경험치가 서로 안 맞는 "찢어진" 상태로 저장된다.
//  - 가끔 방금 한 거래가 저장에 반영되지 않는다(dirty 였는데 누락).
//
// 요구사항
// ----------------------------------------------------------------------------
//  - 스냅샷은 한 캐릭터에 대해 "일관된 한 시점" 을 떠야 한다(필드 간 정합).
//  - 변경분은 반드시 언젠가 저장되어야 한다(유실 금지).
//  - 같은 캐릭터에 대해 더 오래된 스냅샷이 더 새 스냅샷을 덮어쓰면 안 된다.
//
// 과제
// ----------------------------------------------------------------------------
// 이 코드를 코드리뷰하라. (A)~(H) 각 지점의 문제를 지적하고, 동시성/영속화
// 관점에서 정확히 설명한 뒤 수정안과 설계 대안을 제시하라.
// ============================================================================

using System.Collections.Generic;

public class PlayerState
{
    public long PlayerId;
    public long Gold;
    public int  Level;
    public long Exp;
    public bool Dirty;          // (A) 변경됨 표시
}

public struct PlayerSnapshot
{
    public long PlayerId;
    public long Gold;
    public int  Level;
    public long Exp;
}

public class Db
{
    public void Write(PlayerSnapshot s) { /* 네트워크 왕복(지연 가변), 생략 */ }
}

public class PersistenceService
{
    private readonly List<PlayerState> _players;
    private readonly Queue<PlayerSnapshot> _saveQueue = new();
    private readonly object _qlock = new object();
    private readonly Db _db = new Db();

    public PersistenceService(List<PlayerState> players) { _players = players; }

    // 스냅샷 스레드: N초마다 1회 호출
    public void SnapshotTick()
    {
        foreach (var p in _players)
        {
            if (!p.Dirty) continue;             // (B) 변경 없으면 스킵

            var snap = new PlayerSnapshot
            {
                PlayerId = p.PlayerId,
                Gold     = p.Gold,              // (C) 필드별로 읽음(락 없음)
                Level    = p.Level,
                Exp      = p.Exp,
            };

            p.Dirty = false;                    // (D) 저장 전에 dirty 해제

            lock (_qlock) { _saveQueue.Enqueue(snap); }   // (E) 비동기 저장 큐
        }
    }

    // 저장 워커: 여러 스레드가 동시에 돈다
    public void SaveWorker()
    {
        while (true)
        {
            PlayerSnapshot snap;
            lock (_qlock)
            {
                if (_saveQueue.Count == 0) return;
                snap = _saveQueue.Dequeue();
            }
            _db.Write(snap);                    // (F) DB 기록(완료 순서 보장 없음)
        }
    }
}

public static class GameLogic
{
    // 게임 워커 스레드: 상태 변경
    public static void ApplyGold(PlayerState p, long delta)
    {
        p.Gold += delta;        // (G) 락 없음
        p.Dirty = true;         // (H) 변경 표시
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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