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#
내 답안 · 자동 저장

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