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++
// ============================================================================
// [코드리뷰 문제] C++ - 장비 착용/해제 중 강화·분해 끼어듦 (아이템 상태 소유권 경합)
// ----------------------------------------------------------------------------
// 시나리오:
// - 플레이어는 인벤토리(가방)에 아이템을 들고 있고, 각 아이템은 고유 UID 를 가진다.
// - 장비 착용(Equip): 인벤토리에서 아이템을 빼서 장비 슬롯에 넣는다.
// - 장비 해제(Unequip): 장비 슬롯의 아이템을 인벤토리로 되돌린다.
// - 강화(Enchant): 인벤토리 아이템의 강화 수치를 올린다(재료 소모는 생략).
// - 분해(Dismantle): 인벤토리의 아이템을 파괴하고 재료를 돌려준다.
// - 이 요청들은 클라이언트가 거의 동시에(매크로/연타/중복 요청) 보낼 수 있고,
// 서버는 여러 IO 스레드에서 같은 플레이어의 요청을 동시에 처리할 수 있다.
//
// 요구사항:
// - 한 아이템(UID)은 "인벤토리에 있거나, 장비 슬롯에 있거나" 중 정확히 한 곳에만
// 존재해야 한다(복제 금지, 유실 금지).
// - 이미 분해/소모된 아이템을 착용하거나 강화하면 안 된다(해제된 메모리 접근 금지).
// - 강화 수치 증가 / 슬롯 이동 / 분해는 각각 원자적으로 끝나야 한다(부분 적용 금지).
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 깨지는지(동시 인터리빙·수명 포함)
// 설명하고, 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
#include <cstdint>
#include <unordered_map>
#include <memory>
#include <vector>
enum class EquipSlot { Weapon, Armor, Ring };
struct Item {
std::int64_t uid = 0;
int itemId = 0;
int enchantLevel = 0;
bool equippable = true;
};
class PlayerEquipment {
public:
explicit PlayerEquipment(std::vector<Item> initial) {
for (auto& it : initial) {
auto p = std::make_unique<Item>(it);
inventory_.emplace(p->uid, std::move(p));
}
}
// 착용: 인벤토리의 아이템을 슬롯에 넣는다.
bool Equip(std::int64_t uid, EquipSlot slot) {
// (A)
auto it = inventory_.find(uid);
if (it == inventory_.end()) return false;
Item* item = it->second.get();
if (!item->equippable) return false;
equipped_[slot] = item; // 슬롯은 raw 포인터로 참조
inventory_.erase(uid); // 인벤토리에서 제거(소유 unique_ptr 파괴)
return true;
}
// 해제: 장비 슬롯의 아이템을 인벤토리로 되돌린다.
bool Unequip(EquipSlot slot) {
auto it = equipped_.find(slot);
if (it == equipped_.end()) return false;
Item* item = it->second;
equipped_.erase(it);
inventory_[item->uid] = std::make_unique<Item>(*item);
return true;
}
// 강화: 인벤토리 아이템의 강화 수치를 올린다.
bool Enchant(std::int64_t uid) {
// (B)
auto it = inventory_.find(uid);
if (it == inventory_.end()) return false;
Item* item = it->second.get();
int cur = item->enchantLevel;
// ... 재료 소모 / 성공판정 생략 ...
item->enchantLevel = cur + 1;
return true;
}
// 분해: 아이템을 파괴하고 재료를 환급한다.
bool Dismantle(std::int64_t uid) {
// (C)
auto it = inventory_.find(uid);
if (it == inventory_.end()) return false;
Item* item = it->second.get();
RefundMaterials(item);
inventory_.erase(it); // unique_ptr 파괴 → Item 메모리 해제
return true;
}
private:
void RefundMaterials(Item* /*item*/) {
// 분해 재료 환급(외부 인벤토리/재화 가산) — 본 문제에서는 호출 사실만 중요
}
std::unordered_map<std::int64_t, std::unique_ptr<Item>> inventory_;
std::unordered_map<EquipSlot, Item*> equipped_;
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.