51. 장비 젬 소켓 장착/제거의 동시 처리 (젬 복제·유실, C#)
난이도 중 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// 시나리오: MMORPG 장비 소켓(보석/젬) 시스템.
// - 장비(Equipment)에는 여러 개의 소켓 슬롯이 있고, 각 슬롯에 젬(Gem)을 끼우거나 뺄 수 있다.
// - 젬을 소켓에 장착하면 인벤토리의 젬 하나가 소모되어 장비 슬롯으로 옮겨진다.
// - 젬을 소켓에서 제거하면 장비 슬롯의 젬이 다시 인벤토리로 돌아온다.
// - UI 여러 창(장비창/보석 가공창)에서 같은 장비의 같은 슬롯에 대해 장착/제거 요청이
// "거의 동시에" 서버로 들어올 수 있다(더블클릭, 매크로, 창 2개).
//
// 요구사항:
// 1) 하나의 젬은 인벤토리 또는 특정 소켓 슬롯 중 "정확히 한 곳"에만 존재해야 한다(복제/유실 금지).
// 2) 같은 슬롯에 대한 장착/제거가 동시에 처리돼도 인벤토리 수량과 슬롯 상태가 일관돼야 한다.
// 3) 빈 슬롯에 제거를, 찬 슬롯에 장착을 시도하면 거부한다.
//
// 과제: 아래 SocketService 를 검토하고, 동시 요청 시 요구사항 1~2가 깨지는 지점을 찾아
// (A)(B) 마커를 근거로 진단하고, 정확한 수정안과 더 나은 설계를 제시하라.
using System.Collections.Generic;
public sealed class Gem
{
public int GemId;
public int GemType;
}
public sealed class Equipment
{
public int EquipId;
public Gem[] Sockets; // null = 빈 슬롯
public Equipment(int slots) { Sockets = new Gem[slots]; }
}
public sealed class Inventory
{
private readonly List<Gem> _gems = new List<Gem>();
public void Add(Gem g) { _gems.Add(g); }
public Gem Take(int gemType)
{
for (int i = 0; i < _gems.Count; i++)
{
if (_gems[i].GemType == gemType)
{
Gem g = _gems[i];
_gems.RemoveAt(i);
return g;
}
}
return null;
}
public int Count => _gems.Count;
}
public sealed class SocketService
{
private readonly Dictionary<int, Equipment> _equips = new Dictionary<int, Equipment>();
private readonly Inventory _inv;
public SocketService(Inventory inv) { _inv = inv; }
public void Register(Equipment e) { _equips[e.EquipId] = e; }
// 젬 장착: 인벤토리의 젬을 빈 소켓으로 옮긴다.
public bool SocketGem(int equipId, int slot, int gemType)
{
Equipment e = _equips[equipId];
if (e.Sockets[slot] != null) // (A) 슬롯이 비었는지 검사
return false;
Gem g = _inv.Take(gemType);
if (g == null)
return false;
e.Sockets[slot] = g; // (B) 빈 것을 확인한 슬롯에 장착
return true;
}
// 젬 제거: 소켓의 젬을 인벤토리로 되돌린다.
public bool UnsocketGem(int equipId, int slot)
{
Equipment e = _equips[equipId];
Gem g = e.Sockets[slot]; // (A) 슬롯의 젬을 읽어둠
if (g == null)
return false;
_inv.Add(g);
e.Sockets[slot] = null; // (B) 되돌린 뒤 슬롯 비움
return true;
}
} 결함 코드 · C++
// 시나리오: MMORPG 장비 소켓(보석/젬) 시스템 (C++ 서버).
// - 장비에는 소켓 슬롯이 있고, 각 슬롯에 젬을 끼우거나 뺄 수 있다.
// - 장착: 인벤토리의 젬 소유권을 소켓으로 이동. 제거: 소켓의 젬 소유권을 인벤토리로 이동.
// - 같은 장비의 같은 슬롯에 대해 장착/제거 요청이 여러 워커 스레드에서 거의 동시에 들어올 수 있다.
//
// 요구사항:
// 1) 하나의 Gem 객체는 인벤토리 또는 특정 소켓 중 "정확히 한 곳"에서만 소유(소멸 책임)된다.
// 복제(이중 소유)나 유실(양쪽 모두 놓침)이 없어야 한다.
// 2) 동시 장착/제거에도 슬롯 포인터와 인벤토리 벡터가 일관돼야 하며, 이미 해제된 Gem 을
// 가리키는 포인터가 남아서는 안 된다.
// 3) 빈 슬롯 제거, 찬 슬롯 장착은 거부한다.
//
// 과제: 아래 SocketService 를 검토하고, 동시 요청 시 요구사항 1~2가 깨지는 지점을
// (A)(B) 마커를 근거로 진단하고, 정확한 수정안과 더 나은 설계를 제시하라.
//
// 컴파일: g++ -std=c++17 -pthread -fsyntax-only problem.cpp
#include <vector>
#include <unordered_map>
#include <cstddef>
struct Gem
{
int gemId;
int gemType;
};
struct Equipment
{
int equipId;
std::vector<Gem*> sockets; // nullptr = 빈 슬롯
explicit Equipment(int slots) : equipId(0), sockets(slots, nullptr) {}
};
class Inventory
{
public:
void add(Gem* g) { gems_.push_back(g); }
Gem* take(int gemType)
{
for (std::size_t i = 0; i < gems_.size(); ++i)
{
if (gems_[i]->gemType == gemType)
{
Gem* g = gems_[i];
gems_.erase(gems_.begin() + i);
return g;
}
}
return nullptr;
}
private:
std::vector<Gem*> gems_;
};
class SocketService
{
public:
explicit SocketService(Inventory* inv) : inv_(inv) {}
void registerEquip(Equipment* e) { equips_[e->equipId] = e; }
bool socketGem(int equipId, int slot, int gemType)
{
Equipment* e = equips_[equipId];
if (e->sockets[slot] != nullptr) // (A) 슬롯이 비었는지 검사
return false;
Gem* g = inv_->take(gemType);
if (g == nullptr)
return false;
e->sockets[slot] = g; // (B) 빈 것을 확인한 슬롯에 장착
return true;
}
bool unsocketGem(int equipId, int slot)
{
Equipment* e = equips_[equipId];
Gem* g = e->sockets[slot]; // (A) 슬롯의 젬 포인터를 읽어둠
if (g == nullptr)
return false;
inv_->add(g);
e->sockets[slot] = nullptr; // (B) 되돌린 뒤 슬롯 비움
return true;
}
private:
std::unordered_map<int, Equipment*> equips_;
Inventory* inv_;
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.