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#
내 답안 · 자동 저장

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