34. 거래 정산과 거래 취소가 경합하는 상황 (C#)
난이도 중 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// ─────────────────────────────────────────────────────────────
// 시나리오: MMORPG 경매장(거래소).
// - 입찰이 낙찰되면 SettleTrade()가 호출되어 판매자에게 (낙찰가-수수료)를
// 지급하고 수수료는 시스템 금고로 적립한다. 구매자가 낙찰 시 낙찰가는
// 이미 에스크로로 잠겨 있다(여기서는 차감되어 있다고 가정).
// - 운영/신고/오류로 거래를 되돌릴 수 있어 CancelTrade()가 구매자에게
// 낙찰가를 환불하고 아이템을 출품자에게 되돌린다.
// - 정산 워커와 취소(운영/타임아웃) 워커는 서로 다른 스레드에서 같은
// 거래에 대해 동시에 호출될 수 있다.
//
// 요구사항:
// - 한 거래는 "정산" 또는 "취소" 중 정확히 한 번만 종결된다.
// - 정산된 거래는 취소될 수 없고, 취소된 거래는 정산될 수 없다.
// - 모든 골드의 총합(판매자+구매자+시스템 금고+에스크로)은 항상 보존된다.
//
// 과제: (A)(B) 지점을 검토하라. 두 경로가 같은 거래에 대해 동시에 호출될 때
// 무엇이 일어날 수 있는지, 골드 보존이 깨지는 시나리오를 제시하고,
// 요구사항을 만족하도록 수정안을 제시하라.
// ─────────────────────────────────────────────────────────────
using System.Collections.Concurrent;
namespace Exchange
{
public enum TradeState { Pending, Settled, Canceled }
public sealed class Trade
{
public long Id;
public long SellerId;
public long BuyerId;
public long Price; // 낙찰가(에스크로로 잠긴 금액)
public long FeeRate; // 수수료율(만분율: 500 = 5%)
public long ItemId;
public TradeState State = TradeState.Pending;
}
public sealed class Wallet { public long Gold; }
public sealed class ExchangeService
{
private readonly ConcurrentDictionary<long, Wallet> _wallets = new();
private readonly ConcurrentDictionary<long, Trade> _trades = new();
public long SystemVault; // 시스템 수수료 금고
public void Register(Trade t) => _trades[t.Id] = t;
private Wallet W(long id) => _wallets.GetOrAdd(id, _ => new Wallet());
// 낙찰 정산: 판매자에게 (가격-수수료) 지급, 수수료는 금고로 적립
public bool SettleTrade(long tradeId)
{
if (!_trades.TryGetValue(tradeId, out var t)) return false;
// (A)
if (t.State != TradeState.Pending) return false;
long fee = t.Price * t.FeeRate / 10000;
long payout = t.Price - fee;
// (B)
W(t.SellerId).Gold += payout;
SystemVault += fee;
t.State = TradeState.Settled;
return true;
}
// 거래 취소: 구매자에게 낙찰가 환불, 아이템 출품자에게 반환
public bool CancelTrade(long tradeId)
{
if (!_trades.TryGetValue(tradeId, out var t)) return false;
// (A)
if (t.State != TradeState.Pending) return false;
// (B)
W(t.BuyerId).Gold += t.Price;
ReturnItem(t.SellerId, t.ItemId);
t.State = TradeState.Canceled;
return true;
}
private void ReturnItem(long ownerId, long itemId) { /* 인벤토리 반환(생략) */ }
}
} 결함 코드 · C++
// ─────────────────────────────────────────────────────────────
// 시나리오: MMORPG 경매장(거래소).
// - 낙찰되면 SettleTrade()가 판매자에게 (낙찰가-수수료)를 지급하고
// 수수료를 시스템 금고로 적립한다. 낙찰가는 이미 에스크로로 잠겨 있다.
// - 운영/신고/오류로 CancelTrade()가 구매자에게 낙찰가를 환불하고
// 아이템을 출품자에게 되돌린다.
// - 정산 워커와 취소 워커는 서로 다른 스레드에서 같은 거래에 대해
// 동시에 호출될 수 있다.
//
// 요구사항:
// - 한 거래는 "정산" 또는 "취소" 중 정확히 한 번만 종결된다.
// - 정산된 거래는 취소 불가, 취소된 거래는 정산 불가.
// - 모든 골드 총합(판매자+구매자+시스템 금고+에스크로)은 항상 보존.
//
// 과제: (A)(B) 지점을 검토하라. 두 경로가 같은 거래에 동시 호출될 때
// 무엇이 일어나는지, 골드 보존이 깨지는 시나리오를 제시하고 수정안을 제시하라.
// ─────────────────────────────────────────────────────────────
#include <cstdint>
#include <unordered_map>
namespace exchange {
enum class TradeState { Pending, Settled, Canceled };
struct Trade {
int64_t id;
int64_t sellerId;
int64_t buyerId;
int64_t price; // 낙찰가(에스크로)
int64_t feeRate; // 만분율
int64_t itemId;
TradeState state = TradeState::Pending;
};
struct Wallet { int64_t gold = 0; };
class ExchangeService {
public:
void registerTrade(const Trade& t) { trades_[t.id] = t; }
// 낙찰 정산: 판매자에게 (가격-수수료) 지급, 수수료는 금고로
bool settleTrade(int64_t tradeId) {
auto it = trades_.find(tradeId);
if (it == trades_.end()) return false;
Trade& t = it->second;
// (A)
if (t.state != TradeState::Pending) return false;
int64_t fee = t.price * t.feeRate / 10000;
int64_t payout = t.price - fee;
// (B)
wallets_[t.sellerId].gold += payout;
systemVault_ += fee;
t.state = TradeState::Settled;
return true;
}
// 거래 취소: 구매자에게 낙찰가 환불, 아이템 출품자에게 반환
bool cancelTrade(int64_t tradeId) {
auto it = trades_.find(tradeId);
if (it == trades_.end()) return false;
Trade& t = it->second;
// (A)
if (t.state != TradeState::Pending) return false;
// (B)
wallets_[t.buyerId].gold += t.price;
returnItem(t.sellerId, t.itemId);
t.state = TradeState::Canceled;
return true;
}
int64_t systemVault_ = 0;
private:
void returnItem(int64_t /*ownerId*/, int64_t /*itemId*/) { /* 인벤토리 반환(생략) */ }
std::unordered_map<int64_t, Trade> trades_;
std::unordered_map<int64_t, Wallet> wallets_;
};
} // namespace exchange 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.