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++
// ============================================================================
// [코드리뷰 문제] C++ - 경매 낙찰과 동시에 출품자가 아이템을 회수/삭제하려는 상황
// ----------------------------------------------------------------------------
// 시나리오 (콘텐츠/게임로직 · 서버-클라):
// - 플레이어는 경매장에 아이템을 출품(list)한다. 마감 시점에 최고가 입찰자에게
// "낙찰(SettleWin)" 된다: 아이템은 낙찰자에게, 골드는 출품자에게.
// - 출품자는 마감 전까지 출품을 "회수(Recall)" 할 수 있다: 아이템을 출품자에게
// 돌려주고 출품을 취소한다.
// - 마감 타이머에 의한 SettleWin 과 출품자가 누른 Recall 이 거의 동시에 들어올 수
// 있다(서로 다른 스레드).
//
// 요구사항:
// - 한 출품은 "낙찰" 또는 "회수" 중 정확히 하나만 일어나야 한다.
// - 아이템은 정확히 한 명(낙찰자 또는 출품자)에게만 귀속되어야 한다(복제·증발 금지).
// - 골드 정산도 한 번만(낙찰 시 출품자에게 1회) 일어나야 한다.
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 낙찰과 회수가 겹칠 때 어떤 결과가 가능한지
// (아이템이 몇 개가 되는지, 골드가 어떻게 되는지) 설명하고, 수정안과 더 나은 설계를
// 제시하라. (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
#include <cstdint>
#include <unordered_map>
enum class ListingStatus { Active, Sold, Recalled };
struct Listing
{
int64_t listingId;
int64_t sellerId;
int64_t itemId;
int64_t highBidderId; // 0 = 입찰 없음
int highBid;
ListingStatus status = ListingStatus::Active;
};
struct IItemBag { virtual void AddItem(int64_t playerId, int64_t itemId) = 0; virtual ~IItemBag() = default; };
struct IWallet { virtual void AddGold(int64_t playerId, int gold) = 0; virtual ~IWallet() = default; };
class AuctionService
{
std::unordered_map<int64_t, Listing> listings_;
IItemBag& bag_;
IWallet& wallet_;
public:
AuctionService(IItemBag& bag, IWallet& wallet) : bag_(bag), wallet_(wallet) {}
void Register(const Listing& l) { listings_[l.listingId] = l; }
// 마감 타이머가 호출: 최고가 입찰자에게 낙찰
bool SettleWin(int64_t listingId)
{
auto it = listings_.find(listingId);
if (it == listings_.end()) return false;
Listing& l = it->second;
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;
}
// 출품자가 회수: 아이템을 출품자에게 반환
bool Recall(int64_t listingId, int64_t sellerId)
{
auto it = listings_.find(listingId);
if (it == listings_.end()) return false;
Listing& l = it->second;
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#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.