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++
// ============================================================================
// 시나리오
// ----------------------------------------------------------------------------
// 게임서버의 주기적 영속화(스냅샷 저장)다. 캐릭터 상태(골드/레벨/경험치 등)는
// 메모리에 두고 게임 로직이 실시간으로 바꾼다. 데이터 유실을 줄이려고 전용
// "스냅샷 스레드" 가 N초마다 변경된(dirty) 캐릭터를 훑어 현재 상태를 떠서
// 저장 큐에 넣고, 여러 "저장 워커" 가 큐에서 꺼내 DB 에 비동기로 기록한다.
// - 게임 워커 스레드들은 동시에 같은 캐릭터의 상태를 변경한다(골드 가산 등).
// - "스냅샷 스레드는 읽기만 하니 락이 필요 없다"고 가정했다.
// - "한 캐릭터는 한 번에 하나만 저장되겠지"라고 가정했다.
//
// 운영 중 증상(드물고 비결정적, 부하 의존):
// - 가끔 재기동 후 캐릭터 상태가 "과거 값"으로 되돌아간다(최근 변경 유실).
// - 가끔 골드와 레벨/경험치가 서로 안 맞는 "찢어진" 상태로 저장된다.
// - 가끔 방금 한 거래가 저장에 반영되지 않는다(dirty 였는데 누락).
// - 아주 가끔 64비트 골드 값이 말도 안 되는 숫자로 저장된다.
//
// 요구사항
// ----------------------------------------------------------------------------
// - 스냅샷은 한 캐릭터에 대해 "일관된 한 시점" 을 떠야 한다(필드 간 정합).
// - 변경분은 반드시 언젠가 저장되어야 한다(유실 금지).
// - 같은 캐릭터에 대해 더 오래된 스냅샷이 더 새 스냅샷을 덮어쓰면 안 된다.
//
// 과제
// ----------------------------------------------------------------------------
// 이 코드를 코드리뷰하라.
// 1) 동시성/영속화 결함을 모두 찾고 atomic/메모리모델/순서 관점에서 설명하라.
// 2) (A)~(H) 각 지점의 문제를 지적하라.
// 3) 정확하고 효율적으로 수정하라(설계 대안 포함).
// ============================================================================
#include <cstdint>
#include <queue>
#include <vector>
#include <mutex>
struct PlayerState
{
int64_t playerId;
int64_t gold;
int level;
int64_t exp;
bool dirty; // (A) 변경됨 표시
};
struct PlayerSnapshot
{
int64_t playerId;
int64_t gold;
int level;
int64_t exp;
};
class Db
{
public:
void Write(const PlayerSnapshot& s); // 네트워크 왕복(지연 가변), 구현 생략
};
class PersistenceService
{
public:
explicit PersistenceService(std::vector<PlayerState*> players)
: players_(std::move(players)) {}
// 스냅샷 스레드: N초마다 1회 호출
void SnapshotTick()
{
for (PlayerState* p : players_)
{
if (!p->dirty) continue; // (B) 변경 없으면 스킵
PlayerSnapshot snap;
snap.playerId = p->playerId;
snap.gold = p->gold; // (C) 필드별로 읽음(락 없음)
snap.level = p->level;
snap.exp = p->exp;
p->dirty = false; // (D) 저장 전에 dirty 해제
{
std::lock_guard<std::mutex> g(qmtx_);
saveQueue_.push(snap); // (E) 비동기 저장 큐에 적재
}
}
}
// 저장 워커: 여러 스레드가 동시에 돈다
void SaveWorker()
{
for (;;)
{
PlayerSnapshot snap;
{
std::lock_guard<std::mutex> g(qmtx_);
if (saveQueue_.empty()) return;
snap = saveQueue_.front();
saveQueue_.pop();
}
db_.Write(snap); // (F) DB 기록(완료 순서 보장 없음)
}
}
private:
std::vector<PlayerState*> players_;
std::queue<PlayerSnapshot> saveQueue_;
std::mutex qmtx_;
Db db_;
};
// 게임 워커 스레드: 상태 변경
void ApplyGold(PlayerState* p, int64_t delta)
{
p->gold += delta; // (G) 락/atomic 없음
p->dirty = true; // (H) 변경 표시
} 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.