20. 캐릭터 삭제/복구와 진행 중 비동기 저장의 경합
난이도 최상 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 캐릭터 삭제/복구와 진행 중 비동기 저장의 경합
// ----------------------------------------------------------------------------
// 시나리오 (영속화 / 동시성):
// - 캐릭터 상태(골드/레벨 등)는 메모리에서 실시간으로 바뀌고, 비동기 저장 워커가
// 주기적으로 SaveAsync(id) 를 호출해 메모리 스냅샷을 DB 에 UPSERT 한다(느린 I/O).
// - 캐릭터 삭제(Delete)는 소프트 삭제다: DB 에 삭제 표시(MarkDeleted)하고 메모리에서
// 내린 뒤, 일정 유예시간(grace) 후 실제 영구 삭제(Purge=HardDelete)를 예약한다.
// - 유예시간 안에는 복구(Restore)가 가능하다: DB 에서 다시 로드해 메모리에 올린다.
// - 게임 플레이가 진행 중인 상태에서 삭제가 들어올 수 있고, 저장/삭제/복구/퍼지는
// 서로 다른 스레드에서 동시에 일어날 수 있다.
//
// 요구사항:
// - 삭제 후 늦게 끝난 저장이 캐릭터를 되살리면 안 된다(좀비 부활/삭제 유실 금지).
// - 유예 내 복구는 일관된 최신 상태로 살아나야 하고, 이후 예약된 퍼지에 의해
// 복구된 캐릭터가 영구 삭제되면 안 된다.
// - 동시 저장/삭제/복구에도 자료구조 손상이나 정의되지 않은 동작이 없어야 한다.
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 좀비 부활·삭제 유실·복구 캐릭터
// 소멸이 나는지(동시 인터리빙 포함) 설명하고, 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
using System;
using System.Collections.Generic;
public class Character
{
public long Id;
public int Gold;
public int Level;
public bool Deleted;
public long Version; // 존재하지만 일관되게 쓰이지 않음
}
public interface IDatabase
{
void Upsert(Character ch); // 없으면 insert, 있으면 update (느림)
void MarkDeleted(long id); // deleted=true 표시
void ClearDeleted(long id); // deleted=false
Character Load(long id); // 행을 읽어 Character 로
void HardDelete(long id); // 행을 영구 삭제
}
public class CharacterStore
{
private readonly Dictionary<long, Character> _live = new();
private readonly IDatabase _db;
public CharacterStore(IDatabase db) { _db = db; }
// 비동기 저장 워커가 주기적으로 호출
public void SaveAsync(long id)
{
// (A)
if (!_live.TryGetValue(id, out var ch)) return;
var snapshot = new Character { Id = ch.Id, Gold = ch.Gold, Level = ch.Level };
// 느린 DB 쓰기 (수십~수백 ms 소요)
_db.Upsert(snapshot); // UPSERT: 행이 없으면 새로 insert
}
// 소프트 삭제 + 유예 후 퍼지 예약
public void Delete(long id)
{
// (B)
if (_live.TryGetValue(id, out var ch))
ch.Deleted = true;
_live.Remove(id);
_db.MarkDeleted(id);
SchedulePurge(id, graceSec: 600);
}
// 유예 내 복구
public void Restore(long id)
{
// (C)
var ch = _db.Load(id);
ch.Deleted = false;
_db.ClearDeleted(id);
_live[id] = ch;
}
// 유예 만료 후 실제 영구 삭제(타이머 콜백)
private void Purge(long id)
{
// (C)
_db.HardDelete(id);
}
private void SchedulePurge(long id, int graceSec)
{
// graceSec 후 Purge(id) 를 호출하도록 타이머 등록 (구현 생략)
}
} 결함 코드 · C++
// ============================================================================
// [코드리뷰 문제] C++ - 캐릭터 삭제/복구와 진행 중 비동기 저장의 경합
// ----------------------------------------------------------------------------
// 시나리오 (영속화 / 동시성):
// - 캐릭터 상태(골드/레벨 등)는 메모리에서 실시간으로 바뀌고, 비동기 저장 워커가
// 주기적으로 saveAsync(id) 를 호출해 메모리 스냅샷을 DB 에 UPSERT 한다(느린 I/O).
// - 캐릭터 삭제(remove)는 소프트 삭제다: DB 에 삭제 표시(markDeleted)하고 메모리에서
// 내린 뒤, 일정 유예시간(grace) 후 실제 영구 삭제(purge=hardDelete)를 예약한다.
// - 유예시간 안에는 복구(restore)가 가능하다: DB 에서 다시 로드해 메모리에 올린다.
// - 저장/삭제/복구/퍼지는 서로 다른 스레드에서 동시에 일어날 수 있다.
//
// 요구사항:
// - 삭제 후 늦게 끝난 저장이 캐릭터를 되살리면 안 된다(좀비 부활/삭제 유실 금지).
// - 유예 내 복구는 일관된 최신 상태로 살아나야 하고, 이후 예약된 퍼지에 의해
// 복구된 캐릭터가 영구 삭제되면 안 된다.
// - 동시 저장/삭제/복구에도 컨테이너/포인터가 손상되거나 UB 가 없어야 한다.
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 좀비 부활·삭제 유실·복구 캐릭터
// 소멸·UAF 가 나는지(동시 인터리빙 포함) 설명하고, 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
#include <cstdint>
#include <unordered_map>
struct Character {
int64_t id;
int gold;
int level;
bool deleted;
int64_t version; // 존재하지만 일관되게 쓰이지 않음
};
class IDatabase {
public:
virtual ~IDatabase() = default;
virtual void upsert(const Character& ch) = 0; // 없으면 insert, 있으면 update (느림)
virtual void markDeleted(int64_t id) = 0;
virtual void clearDeleted(int64_t id) = 0;
virtual Character load(int64_t id) = 0;
virtual void hardDelete(int64_t id) = 0;
};
class CharacterStore {
public:
explicit CharacterStore(IDatabase* db) : db_(db) {}
// 비동기 저장 워커가 주기적으로 호출
void saveAsync(int64_t id) {
// (A)
auto it = live_.find(id);
if (it == live_.end()) return;
Character* ch = it->second; // raw 포인터 보유
Character snapshot{ch->id, ch->gold, ch->level, false, 0};
// 느린 DB 쓰기 (수십~수백 ms)
db_->upsert(snapshot); // UPSERT: 행이 없으면 새로 insert
}
// 소프트 삭제 + 유예 후 퍼지 예약
void remove(int64_t id) {
// (B)
auto it = live_.find(id);
if (it != live_.end()) {
it->second->deleted = true;
delete it->second; // 메모리 해제
live_.erase(it);
}
db_->markDeleted(id);
schedulePurge(id, /*graceSec*/ 600);
}
// 유예 내 복구
void restore(int64_t id) {
// (C)
Character* ch = new Character(db_->load(id));
ch->deleted = false;
db_->clearDeleted(id);
live_[id] = ch;
}
// 유예 만료 후 실제 영구 삭제(타이머 콜백)
void purge(int64_t id) {
// (C)
db_->hardDelete(id);
}
private:
void schedulePurge(int64_t id, int graceSec) { /* 타이머 등록 (구현 생략) */ }
std::unordered_map<int64_t, Character*> live_;
IDatabase* db_;
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.