46. 퀘스트 보상 수령과 포기/재도전의 비원자 상태 전이 (이중 지급·재수령) (C#)

난이도 하 해설 보기 →

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

결함 코드 · C#
// 시나리오: MMORPG 퀘스트 시스템.
// 퀘스트는 NotStarted → InProgress → Completed → Rewarded 상태를 가진다.
// 플레이어는 "완료된 퀘스트의 보상 수령(ClaimReward)"과
// "퀘스트 포기/재도전(AbandonQuest)"을 각각 다른 패킷으로 보낼 수 있다.
//
// 요구사항:
//  1) 보상은 퀘스트당 정확히 한 번만 지급되어야 한다.
//  2) 포기하면 진행도가 초기화되어 처음부터 다시 진행할 수 있어야 한다.
//  3) 클라이언트가 "수령"과 "포기"를 거의 동시에 보내도(더블클릭/매크로/네트워크 재전송)
//     보상이 두 번 나가거나, 이미 받은 보상을 재도전으로 다시 받을 수 없어야 한다.
//
// 아래 서버 코드에는 이 요구사항을 위반할 수 있는 지점이 있다.
// 서로 다른 워커 스레드가 같은 플레이어의 두 요청을 동시에 처리한다고 가정하고,
// (A)와 (B) 지점을 중심으로 문제를 진단하고 수정안을 제시하라.

using System.Collections.Generic;

public enum QuestState { NotStarted, InProgress, Completed, Rewarded }

public class QuestProgress
{
    public int QuestId;
    public QuestState State;
}

public class RewardTable
{
    public IReadOnlyList<(int itemId, int count)> Rewards(int questId) => _map[questId];
    private readonly Dictionary<int, List<(int, int)>> _map = new();
    public void Register(int questId, List<(int, int)> r) => _map[questId] = r;
}

public class QuestService
{
    private readonly Dictionary<int, QuestProgress> _quests = new(); // questId -> progress
    private readonly RewardTable _rewards;
    private readonly Inventory _inventory;

    public QuestService(RewardTable rewards, Inventory inventory)
    {
        _rewards = rewards;
        _inventory = inventory;
    }

    public bool ClaimReward(int questId)
    {
        if (!_quests.TryGetValue(questId, out var q))
            return false;

        // (A) 상태 확인 후 지급까지의 처리
        if (q.State != QuestState.Completed)
            return false;

        foreach (var (itemId, count) in _rewards.Rewards(questId))
            _inventory.Add(itemId, count);

        q.State = QuestState.Rewarded;
        return true;
    }

    public bool AbandonQuest(int questId)
    {
        if (!_quests.TryGetValue(questId, out var q))
            return false;

        // (B) 포기 처리: 진행도 초기화
        q.State = QuestState.NotStarted;
        return true;
    }

    public void Seed(int questId, QuestState state)
        => _quests[questId] = new QuestProgress { QuestId = questId, State = state };
}

public class Inventory
{
    private readonly Dictionary<int, int> _items = new();
    private readonly object _lock = new();
    public void Add(int itemId, int count)
    {
        lock (_lock) { _items.TryGetValue(itemId, out var c); _items[itemId] = c + count; }
    }
    public int CountOf(int itemId)
    {
        lock (_lock) { _items.TryGetValue(itemId, out var c); return c; }
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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