14. 퀘스트 완료 보상 수령 (중복 수령 방지 / 멱등성)

난이도 하 해설 보기 →

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

결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 퀘스트 완료 보상 수령 (중복 수령 방지 / 멱등성)
// ----------------------------------------------------------------------------
// 시나리오:
//   - 플레이어가 퀘스트를 완료하면 상태가 Completed 가 되고, "보상 받기" 버튼을
//     누르면 클라이언트가 C_ClaimQuestReward(questId) 패킷을 보낸다.
//   - 서버는 보상(골드/아이템)을 지급하고 퀘스트 상태를 Claimed 로 바꾼다.
//   - 보상은 단 한 번만 지급되어야 한다(같은 퀘스트로 두 번 받으면 안 됨).
//   - 클라이언트는 신뢰할 수 없고, 버튼 더블클릭/매크로/네트워크 재전송으로
//     같은 C_ClaimQuestReward 패킷이 거의 동시에 두 번 이상 도착할 수 있다.
//   - 같은 플레이어의 패킷이라도 서로 다른 IO 스레드에서 동시에 처리될 수 있다.
//
// 요구사항:
//   - 보상 지급 여부는 서버가 권위 있게 판정한다.
//   - 어떤 동시성/재전송 상황에서도 보상은 정확히 한 번만 지급되어야 한다.
//   - 보상 지급(골드/아이템)과 상태 전이(Claimed)는 함께 일어나야 한다.
//
// 과제:
//   이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 악용되는지 설명하고,
//   수정안과 더 나은 설계를 제시하라.
//   (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================

using System;
using System.Collections.Generic;

public enum QuestState { InProgress, Completed, Claimed }

public class QuestReward
{
    public long Gold;
    public int  ItemId;
    public int  ItemCount;
}

public class Player
{
    public long Id;
    public long Gold;
    // questId -> 상태
    public Dictionary<int, QuestState> Quests = new Dictionary<int, QuestState>();
    public Inventory Bag = new Inventory();
}

public class Inventory
{
    public void Add(int itemId, int count) { /* 가방에 추가 (생략) */ }
}

public class QuestService
{
    private readonly Dictionary<int, QuestReward> _rewards;     // questId -> 보상 정의
    private readonly Dictionary<long, Player>      _players;

    public QuestService(Dictionary<int, QuestReward> rewards, Dictionary<long, Player> players)
    {
        _rewards = rewards;
        _players = players;
    }

    // C_ClaimQuestReward 처리. 지급 성공 시 true.
    public bool ClaimReward(long playerId, int questId)
    {
        Player p = _players[playerId];

        // (A) 수령 가능 여부 판정
        if (p.Quests[questId] != QuestState.Completed)
            return false;

        QuestReward r = _rewards[questId];

        // (B) 보상 지급
        p.Gold += r.Gold;
        p.Bag.Add(r.ItemId, r.ItemCount);

        // (C) 상태 전이
        p.Quests[questId] = QuestState.Claimed;

        return true;
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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