18. 장비 착용/해제 중 강화·분해 끼어듦 (아이템 상태 소유권 경합)

난이도 중 해설 보기 →

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

결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 장비 착용/해제 중 강화·분해 끼어듦 (아이템 상태 소유권 경합)
// ----------------------------------------------------------------------------
// 시나리오:
//   - 플레이어는 인벤토리(가방)에 아이템을 들고 있고, 각 아이템은 고유 UID 를 가진다.
//   - 장비 착용(Equip): 인벤토리에서 아이템을 빼서 장비 슬롯에 넣는다.
//   - 장비 해제(Unequip): 장비 슬롯의 아이템을 인벤토리로 되돌린다.
//   - 강화(Enchant): 인벤토리 아이템의 강화 수치를 올린다(재료 소모는 생략).
//   - 분해(Dismantle): 인벤토리의 아이템을 파괴하고 재료를 돌려준다.
//   - 이 요청들은 클라이언트가 거의 동시에(매크로/연타/중복 요청) 보낼 수 있고,
//     서버는 여러 IO 스레드에서 같은 플레이어의 요청을 동시에 처리할 수 있다.
//
// 요구사항:
//   - 한 아이템(UID)은 "인벤토리에 있거나, 장비 슬롯에 있거나" 중 정확히 한 곳에만
//     존재해야 한다(복제 금지, 유실 금지).
//   - 이미 분해/소모된 아이템을 착용하거나 강화하면 안 된다.
//   - 강화 수치 증가 / 슬롯 이동 / 분해는 각각 원자적으로 끝나야 한다(부분 적용 금지).
//
// 과제:
//   이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 깨지는지(동시 인터리빙 포함)
//   설명하고, 수정안과 더 나은 설계를 제시하라.
//   (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================

using System;
using System.Collections.Generic;

public enum EquipSlot { Weapon, Armor, Ring }

public class Item
{
    public long Uid;
    public int  ItemId;
    public int  EnchantLevel;
    public bool Equippable;
}

public class PlayerEquipment
{
    // 인벤토리: UID -> 아이템
    private readonly Dictionary<long, Item> _inventory = new();
    // 장비 슬롯: 슬롯 -> 착용 중인 아이템
    private readonly Dictionary<EquipSlot, Item> _equipped = new();

    public PlayerEquipment(IEnumerable<Item> initial)
    {
        foreach (var it in initial) _inventory[it.Uid] = it;
    }

    // 착용: 인벤토리의 아이템을 슬롯에 넣는다.
    public bool Equip(long uid, EquipSlot slot)
    {
        // (A)
        if (!_inventory.TryGetValue(uid, out var item)) return false;
        if (!item.Equippable) return false;

        _equipped[slot] = item;
        _inventory.Remove(uid);
        return true;
    }

    // 해제: 장비 슬롯의 아이템을 인벤토리로 되돌린다.
    public bool Unequip(EquipSlot slot)
    {
        if (!_equipped.TryGetValue(slot, out var item)) return false;
        _equipped.Remove(slot);
        _inventory[item.Uid] = item;
        return true;
    }

    // 강화: 인벤토리 아이템의 강화 수치를 올린다.
    public bool Enchant(long uid)
    {
        // (B)
        if (!_inventory.TryGetValue(uid, out var item)) return false;
        int cur = item.EnchantLevel;
        // ... 재료 소모 / 성공판정 생략 ...
        item.EnchantLevel = cur + 1;
        return true;
    }

    // 분해: 아이템을 파괴하고 재료를 환급한다.
    public bool Dismantle(long uid)
    {
        // (C)
        if (!_inventory.TryGetValue(uid, out var item)) return false;
        _inventory.Remove(uid);
        RefundMaterials(item);
        return true;
    }

    private void RefundMaterials(Item item)
    {
        // 분해 재료 환급(외부 인벤토리/재화 가산) — 본 문제에서는 호출 사실만 중요
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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