3. 플레이어 간 아이템 직거래(트레이드)

난이도 상 해설 보기 →

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

결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 플레이어 간 아이템 직거래(트레이드)
// ----------------------------------------------------------------------------
// 시나리오:
//   - 두 플레이어가 거래창을 열고 각자 아이템을 올린 뒤(Offer),
//     양쪽이 Confirm 을 누르면 거래가 체결(Commit)된다.
//   - 거래 도중 한쪽이 접속을 끊거나(취소), 다른 거래를 동시에 열 수 있다.
//   - 아이템은 고유 인스턴스 ID(itemUid)를 가진다(장비 등 비스택 아이템).
//
// 요구사항:
//   - 거래 체결은 원자적이어야 한다(둘 다 받거나, 둘 다 못 받거나).
//   - 같은 아이템을 두 거래에 동시에 올릴 수 없다(중복 거래 금지).
//   - 거래 도중 한쪽 로그아웃/취소 시 올린 아이템은 원래 주인에게 돌아간다.
//   - 여러 IO 스레드가 거래 패킷을 동시에 처리한다.
//
// 과제:
//   이 코드의 잠재적 문제를 모두 찾고, 왜 문제인지 설명하고,
//   수정안과 더 나은 설계를 제시하라.
//   (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================

using System;
using System.Collections.Generic;

public class Item
{
    public long Uid { get; }
    public int  ItemId { get; }
    public Item(long uid, int itemId) { Uid = uid; ItemId = itemId; }
}

public class Player
{
    public long Id { get; }
    public readonly object Lock = new object();
    public Dictionary<long, Item> Inventory = new Dictionary<long, Item>();
    public Player(long id) { Id = id; }
}

public class Trade
{
    public Player A, B;
    public List<long> OfferA = new List<long>();   // A 가 올린 itemUid
    public List<long> OfferB = new List<long>();
    public bool ConfirmA, ConfirmB;
    public bool Committed;
}

public class TradeService
{
    private readonly Dictionary<long, Player> _players;
    private readonly object _lock = new object();
    private readonly Dictionary<(long, long), Trade> _trades = new Dictionary<(long, long), Trade>();

    public TradeService(Dictionary<long, Player> players) { _players = players; }

    public Trade OpenTrade(long aId, long bId)
    {
        var t = new Trade { A = _players[aId], B = _players[bId] };
        lock (_lock) { _trades[(aId, bId)] = t; }       // (A)
        return t;
    }

    // 플레이어가 거래창에 아이템을 올림
    public bool Offer(Trade t, long playerId, long itemUid)
    {
        Player p = (t.A.Id == playerId) ? t.A : t.B;
        // (B) 본인 인벤토리에 있는지 확인
        if (!p.Inventory.ContainsKey(itemUid))
            return false;

        if (t.A.Id == playerId) t.OfferA.Add(itemUid);  // (C)
        else                    t.OfferB.Add(itemUid);
        // 한쪽이 새로 아이템을 올리면 기존 확인은 무효화돼야 하지만...
        return true;
    }

    public void Confirm(Trade t, long playerId)
    {
        if (t.A.Id == playerId) t.ConfirmA = true;       // (D)
        else                    t.ConfirmB = true;

        if (t.ConfirmA && t.ConfirmB)
            Commit(t);
    }

    private void Commit(Trade t)
    {
        if (t.Committed) return;
        t.Committed = true;

        // (E) A 의 아이템을 B 에게
        foreach (var uid in t.OfferA)
        {
            Item item = t.A.Inventory[uid];
            t.A.Inventory.Remove(uid);
            t.B.Inventory[uid] = item;
        }
        // (F) B 의 아이템을 A 에게
        foreach (var uid in t.OfferB)
        {
            Item item = t.B.Inventory[uid];
            t.B.Inventory.Remove(uid);
            t.A.Inventory[uid] = item;
        }
    }

    // 한쪽이 로그아웃/취소
    public void Cancel(Trade t)
    {
        lock (_lock) { _trades.Remove((t.A.Id, t.B.Id)); }  // (G)
        // 올린 아이템은 인벤토리에서 빠진 적이 없으니 그냥 둔다 (H)
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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