29. 경매 낙찰과 출품 회수의 동시 처리 (이중 귀속 방지)

난이도 하 해설 보기 →

결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커 (A)(B) 는 주목 위치 힌트다.

결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 경매 낙찰과 동시에 출품자가 아이템을 회수/삭제하려는 상황
// ----------------------------------------------------------------------------
// 시나리오 (콘텐츠/게임로직 · 서버-클라):
//   - 플레이어는 경매장에 아이템을 출품(list)한다. 다른 플레이어가 입찰해 마감 시점에
//     최고가 입찰자에게 "낙찰(SettleWin)" 된다: 아이템은 낙찰자에게, 골드는 출품자에게.
//   - 출품자는 마감 전까지 출품을 "회수(Recall)" 할 수 있다: 아이템을 출품자에게 돌려주고
//     출품을 취소한다.
//   - 마감 타이머에 의한 SettleWin 과 출품자가 누른 Recall 이 거의 동시에 들어올 수 있다
//     (서로 다른 요청/스레드).
//
// 요구사항:
//   - 한 출품은 "낙찰" 또는 "회수" 중 정확히 하나만 일어나야 한다.
//   - 아이템은 정확히 한 명(낙찰자 또는 출품자)에게만 귀속되어야 한다(복제·증발 금지).
//   - 골드 정산도 한 번만(낙찰 시 출품자에게 1회) 일어나야 한다.
//
// 과제:
//   이 코드의 잠재적 문제를 모두 찾고, 낙찰과 회수가 겹칠 때 어떤 결과가 가능한지
//   (아이템이 몇 개가 되는지, 골드가 어떻게 되는지) 설명하고, 수정안과 더 나은 설계를
//   제시하라. (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================

using System;
using System.Collections.Generic;

public enum ListingStatus { Active, Sold, Recalled }

public class AuctionListing
{
    public long          ListingId;
    public long          SellerId;
    public long          ItemId;
    public long          HighBidderId;   // 0 = 입찰 없음
    public int           HighBid;
    public ListingStatus Status = ListingStatus.Active;
}

public interface IItemBag
{
    void AddItem(long playerId, long itemId);
}

public interface IWallet
{
    void AddGold(long playerId, int gold);
}

public class AuctionService
{
    private readonly Dictionary<long, AuctionListing> _listings = new();
    private readonly IItemBag _bag;
    private readonly IWallet  _wallet;

    public AuctionService(IItemBag bag, IWallet wallet) { _bag = bag; _wallet = wallet; }

    public void Register(AuctionListing l) => _listings[l.ListingId] = l;

    // 마감 타이머가 호출: 최고가 입찰자에게 낙찰
    public bool SettleWin(long listingId)
    {
        if (!_listings.TryGetValue(listingId, out var l)) return false;
        if (l.Status != ListingStatus.Active) return false;   // (A)
        if (l.HighBidderId == 0) return false;                // 입찰 없음

        _bag.AddItem(l.HighBidderId, l.ItemId);               // (B) 아이템 → 낙찰자
        _wallet.AddGold(l.SellerId, l.HighBid);               //     골드 → 출품자
        l.Status = ListingStatus.Sold;
        return true;
    }

    // 출품자가 회수: 아이템을 출품자에게 반환
    public bool Recall(long listingId, long sellerId)
    {
        if (!_listings.TryGetValue(listingId, out var l)) return false;
        if (l.SellerId != sellerId) return false;
        if (l.Status != ListingStatus.Active) return false;   // (C)

        _bag.AddItem(l.SellerId, l.ItemId);                   // (D) 아이템 → 출품자
        l.Status = ListingStatus.Recalled;
        return true;
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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