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++
// ============================================================================
// [코드리뷰 문제] C++ - 플레이어 간 아이템 직거래(트레이드)
// ----------------------------------------------------------------------------
// 시나리오:
// - 두 플레이어가 거래창을 열고 각자 아이템을 올린 뒤(Offer),
// 양쪽이 Confirm 을 누르면 거래가 체결(Commit)된다.
// - 거래 도중 한쪽이 접속을 끊거나(취소), 다른 거래를 동시에 열 수 있다.
// - 아이템은 고유 인스턴스 ID(itemUid)를 가진다(장비 등 비스택 아이템).
//
// 요구사항:
// - 거래 체결은 원자적이어야 한다(둘 다 받거나, 둘 다 못 받거나).
// - 같은 아이템을 두 거래에 동시에 올릴 수 없다(중복 거래 금지).
// - 거래 도중 한쪽 로그아웃/취소 시 올린 아이템은 원래 주인에게 돌아간다.
// - 여러 IO 스레드가 거래 패킷을 동시에 처리한다.
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜 문제인지 설명하고,
// 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
#include <cstdint>
#include <vector>
#include <map>
#include <unordered_map>
#include <mutex>
struct Item {
int64_t uid;
int32_t itemId;
};
struct Player {
int64_t id;
std::mutex lock;
std::unordered_map<int64_t, Item> inventory; // uid -> Item
};
struct Trade {
Player* a = nullptr;
Player* b = nullptr;
std::vector<int64_t> offerA; // a 가 올린 itemUid
std::vector<int64_t> offerB;
bool confirmA = false;
bool confirmB = false;
bool committed = false;
};
class TradeService {
public:
explicit TradeService(std::unordered_map<int64_t, Player*>& players)
: players_(players) {}
Trade* OpenTrade(int64_t aId, int64_t bId) {
Trade* t = new Trade();
t->a = players_.at(aId);
t->b = players_.at(bId);
{
std::lock_guard<std::mutex> lk(lock_);
trades_[{aId, bId}] = t; // (A)
}
return t;
}
// 플레이어가 거래창에 아이템을 올림
bool Offer(Trade* t, int64_t playerId, int64_t itemUid) {
Player* p = (t->a->id == playerId) ? t->a : t->b;
// (B) 본인 인벤토리에 있는지 확인
if (p->inventory.find(itemUid) == p->inventory.end())
return false;
if (t->a->id == playerId) t->offerA.push_back(itemUid); // (C)
else t->offerB.push_back(itemUid);
// 한쪽이 새로 아이템을 올리면 기존 확인은 무효화돼야 하지만...
return true;
}
void Confirm(Trade* t, int64_t playerId) {
if (t->a->id == playerId) t->confirmA = true; // (D)
else t->confirmB = true;
if (t->confirmA && t->confirmB)
Commit(t);
}
// 한쪽이 로그아웃/취소
void Cancel(Trade* t) {
{
std::lock_guard<std::mutex> lk(lock_);
trades_.erase({t->a->id, t->b->id}); // (G)
}
// 올린 아이템은 인벤토리에서 빠진 적이 없으니 그냥 둔다 (H)
delete t;
}
private:
void Commit(Trade* t) {
if (t->committed) return;
t->committed = true;
// (E) A 의 아이템을 B 에게
for (int64_t uid : t->offerA) {
Item item = t->a->inventory[uid];
t->a->inventory.erase(uid);
t->b->inventory[uid] = item;
}
// (F) B 의 아이템을 A 에게
for (int64_t uid : t->offerB) {
Item item = t->b->inventory[uid];
t->b->inventory.erase(uid);
t->a->inventory[uid] = item;
}
}
std::unordered_map<int64_t, Player*>& players_;
std::mutex lock_;
std::map<std::pair<int64_t, int64_t>, Trade*> trades_;
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.