24. 귀속/거래불가 아이템이 교환·판매·우편 경로로 새는 상황

난이도 하 해설 보기 →

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

결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 귀속/거래불가 아이템이 교환·판매·우편 경로로 새는 상황
// ----------------------------------------------------------------------------
// 시나리오 (콘텐츠/게임로직 · 서버-클라):
//   - 아이템에는 "귀속(BindOnPickup/BindOnEquip)" 과 "거래 가능 여부(Tradable)"
//     속성이 있다. 귀속된 아이템·거래불가 아이템은 다른 플레이어에게 넘어가면 안 된다
//     (직거래·우편 첨부·경매 출품 금지). 단, 상점 판매(NPC) 는 정책상 일부 허용될 수 있다.
//   - 클라이언트는 거래/우편/경매 요청 패킷에 itemId 와 함께 부가 정보를 담아 보낸다.
//   - 서버는 이 요청을 받아 아이템 이동을 수행한다.
//
// 요구사항:
//   - 귀속/거래불가 아이템은 직거래·우편 첨부·경매 출품 경로로 절대 이전되면 안 된다.
//   - "이 아이템이 거래 가능한지" 는 서버 권위 데이터로 판정해야 한다(클라 값 신뢰 금지).
//   - 이동 시 소유권 검증(요청자가 실제 소유자인지)도 필요하다.
//
// 과제:
//   이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 귀속·거래불가 아이템이 다른 플레이어에게
//   새어나가는지 설명하고, 수정안과 더 나은 설계를 제시하라.
//   (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================

using System;
using System.Collections.Generic;

public class Item
{
    public long   ItemId;
    public int    TemplateId;
    public long   OwnerId;
    public bool   Bound;        // 귀속 여부(서버 권위)
    public bool   Tradable;     // 거래 가능 여부(서버 권위, 템플릿에서 옴)
}

// 클라가 보낸 거래 요청
public class TradeRequest
{
    public long FromPlayer;
    public long ToPlayer;
    public long ItemId;
    public bool ClientSaysTradable;   // 클라가 "이건 거래 가능"이라고 보낸 값
}

public class ItemTransferService
{
    private readonly Dictionary<long, Item> _items;          // itemId -> Item
    private readonly Func<long, long, bool> _giveToPlayer;   // (toPlayer, itemId) 인벤토리 추가

    public ItemTransferService(Dictionary<long, Item> items, Func<long, long, bool> giveToPlayer)
    {
        _items = items;
        _giveToPlayer = giveToPlayer;
    }

    // 직거래: From -> To 로 아이템 이전
    public bool Trade(TradeRequest req)
    {
        if (!_items.TryGetValue(req.ItemId, out var item)) return false;

        // (A) 거래 가능 여부를 클라가 보낸 값으로 판정
        if (!req.ClientSaysTradable) return false;

        item.OwnerId = req.ToPlayer;
        _giveToPlayer(req.ToPlayer, req.ItemId);
        return true;
    }

    // 우편 첨부: 아이템을 우편으로 다른 플레이어에게 보냄
    public bool AttachToMail(long fromPlayer, long toPlayer, long itemId)
    {
        if (!_items.TryGetValue(itemId, out var item)) return false;

        // (B) 귀속/거래불가 검증 없이 그대로 첨부
        item.OwnerId = toPlayer;
        _giveToPlayer(toPlayer, itemId);
        return true;
    }

    // 경매 출품: 아이템을 경매장에 올림(이후 낙찰자에게 이전)
    public bool ListOnAuction(long sellerId, long itemId)
    {
        if (!_items.TryGetValue(itemId, out var item)) return false;

        // (C) 소유자 확인도, 귀속/거래불가 검증도 없음
        item.OwnerId = 0;   // 0 = 경매장 보관
        return true;
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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