15. 던전 클리어 보상 지급 중 인스턴스 만료/정리 경합 (C#)
난이도 상 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 던전 클리어 보상 지급 중 인스턴스 만료/정리 경합 (수명/정합성)
// ----------------------------------------------------------------------------
// 시나리오 (동시성):
// - 파티가 던전 보스를 잡으면 인스턴스가 "클리어" 상태가 되고, 보상 지급
// 루틴이 인스턴스의 멤버들을 순회하며 골드/아이템을 비동기로 지급한다.
// - 한편 별도의 정리 타이머가 "빈/유휴/만료" 인스턴스를 주기적으로 스윕해
// 맵에서 제거하고 인스턴스의 자원(DB 핸들 등)을 정리(Dispose)한다.
// - 보상 지급은 DB I/O 등으로 시간이 걸려, 그 사이 마지막 멤버가 나가거나
// 유휴 타임아웃이 차서 정리 타이머가 같은 인스턴스를 회수할 수 있다.
// - 보상 지급 루틴과 정리 타이머는 서로 다른 스레드에서 동작한다.
//
// 요구사항:
// - 보상 지급이 진행 중인 인스턴스는 정리되면 안 되고, 정리 중인 인스턴스에
// 보상 지급이 끼어들어 이미 해제된 자원을 쓰면 안 된다.
// - 보상은 정확히 한 번 지급되어야 한다(정리와 겹쳐 유실/중복 금지).
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 깨지는지(경합 인터리빙 포함)
// 설명하고, 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
using System;
using System.Collections.Generic;
public struct Reward { public long Gold; public int ItemId; public int ItemCount; }
public class DungeonInstance : IDisposable
{
public long Id;
public bool Cleared;
public bool RewardGranted;
public List<long> Members = new List<long>(); // 현재 인스턴스 안의 플레이어들
public Reward Reward;
private bool _disposed;
public void GrantTo(long playerId)
{
// DB I/O 포함 — 수십~수백 ms 걸릴 수 있음 (단순화)
if (_disposed) throw new ObjectDisposedException(nameof(DungeonInstance));
SlowGrant(playerId, Reward);
}
private void SlowGrant(long pid, Reward r) { /* DB 지급 (생략) */ }
public void Dispose() { _disposed = true; /* DB 핸들 등 정리 */ }
}
public class DungeonManager
{
private readonly Dictionary<long, DungeonInstance> _instances = new();
// 보스 처치 시 호출 — 클리어 보상 지급 시작 (별도 스레드에서 비동기 실행)
public void OnBossKilled(long instanceId)
{
DungeonInstance inst = _instances[instanceId]; // (A) 조회
inst.Cleared = true;
// (B) 멤버 순회하며 보상 지급 (느린 작업)
foreach (long pid in inst.Members)
inst.GrantTo(pid);
inst.RewardGranted = true;
}
// 정리 타이머가 주기적으로 호출 — 빈/유휴/만료 인스턴스 회수
public void SweepIdle()
{
foreach (var kv in _instances)
{
var inst = kv.Value;
if (inst.Members.Count == 0) // (C) 비었으면 회수
{
inst.Dispose(); // 자원 정리
_instances.Remove(kv.Key);
}
}
}
public void AddInstance(DungeonInstance inst) => _instances[inst.Id] = inst;
} 결함 코드 · C++
// ============================================================================
// [코드리뷰 문제] C++ - 던전 클리어 보상 지급 중 인스턴스 만료/정리 경합 (수명/UAF)
// ----------------------------------------------------------------------------
// 시나리오 (동시성/메모리):
// - 파티가 던전 보스를 잡으면 인스턴스가 "클리어" 상태가 되고, 보상 지급
// 루틴이 인스턴스의 멤버들을 순회하며 골드/아이템을 비동기로 지급한다.
// - 한편 별도의 정리(GC) 타이머가 "빈/유휴/만료" 인스턴스를 주기적으로 스윕해
// 맵에서 제거하고 인스턴스 객체를 파괴한다(메모리 회수).
// - 보상 지급은 DB I/O 등으로 시간이 걸려, 그 사이 마지막 멤버가 나가거나
// 유휴 타임아웃이 차서 정리 타이머가 같은 인스턴스를 회수할 수 있다.
// - 보상 지급 루틴과 정리 타이머는 서로 다른 스레드에서 동작한다.
//
// 요구사항:
// - 보상 지급이 진행 중인 인스턴스는 회수되면 안 되고, 회수 중인 인스턴스에
// 보상 지급이 끼어들어 해제된 메모리를 건드리면 안 된다(UAF 금지).
// - 보상은 정확히 한 번 지급되어야 한다(정리와 겹쳐 유실/중복 금지).
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 깨지는지(경합 인터리빙·수명
// 포함) 설명하고, 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
#include <cstdint>
#include <unordered_map>
#include <vector>
#include <thread>
struct Reward { int64_t gold; int itemId; int itemCount; };
class DungeonInstance {
public:
int64_t id;
bool cleared = false;
bool rewardGranted = false;
std::vector<int64_t> members; // 현재 인스턴스 안의 플레이어들
Reward reward;
void GrantTo(int64_t playerId) {
// DB I/O 포함 — 수십~수백 ms 걸릴 수 있음 (단순화)
SlowGrant(playerId, reward);
}
private:
void SlowGrant(int64_t, const Reward&) { /* DB 지급 (생략) */ }
};
class DungeonManager {
public:
// 보스 처치 시 호출 — 클리어 보상 지급 시작 (별도 스레드에서 비동기 실행)
void OnBossKilled(int64_t instanceId) {
DungeonInstance* inst = instances_[instanceId]; // (A) 조회
inst->cleared = true;
// (B) 멤버 순회하며 보상 지급 (느린 작업)
for (int64_t pid : inst->members)
inst->GrantTo(pid);
inst->rewardGranted = true;
}
// 정리 타이머가 주기적으로 호출 — 빈/유휴/만료 인스턴스 회수
void SweepIdle() {
for (auto it = instances_.begin(); it != instances_.end(); ) {
DungeonInstance* inst = it->second;
if (inst->members.empty()) { // (C) 비었으면 회수
delete inst; // 객체 파괴
it = instances_.erase(it);
} else {
++it;
}
}
}
void AddInstance(DungeonInstance* inst) { instances_[inst->id] = inst; }
private:
std::unordered_map<int64_t, DungeonInstance*> instances_;
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.