20. 거래 도중 한쪽이 로그아웃/접속종료되어 롤백이 필요한 상황
난이도 상 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 거래 도중 한쪽이 로그아웃/접속종료되어 롤백이 필요한 상황
// ----------------------------------------------------------------------------
// 시나리오 (세션/네트워크 · 서버-클라):
// - 두 플레이어가 직거래 창을 열고 각자 아이템/골드를 올린 뒤, 양쪽이 "확정(confirm)" 하면
// 서버가 교환을 commit 한다(A의 물건은 B에게, B의 물건은 A에게).
// - commit 직전/도중에 한쪽 세션이 끊길 수 있다(자발적 로그아웃 또는 연결 끊김 -> OnDisconnect).
// 이 경우 거래는 안전하게 취소(롤백)되어야 하고, 이미 일부가 적용됐다면 원자적으로 되돌려야 한다.
// - OnConfirm 과 OnDisconnect 는 서로 다른 스레드(요청 처리 / 네트워크 콜백)에서 들어온다.
//
// 요구사항:
// - 교환은 원자적이어야 한다: "A의 아이템만 넘어가고 B의 골드는 안 넘어온" 부분 적용 금지
// (아이템 증발/복제 금지).
// - commit 과 disconnect-취소가 동시에 일어나도 거래는 정확히 한 번만 commit 되거나
// 정확히 한 번만 취소되어야 한다(이중 commit/이중 롤백/commit+취소 동시 금지).
// - 접속종료 시 진행 중 거래가 방치되면 안 된다(세션만 정리하고 거래를 남기면 누수/복제).
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 부분 적용(복제/증발)·이중 처리·좀비 거래가
// 동시 인터리빙으로 발생하는지 설명하고, 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
using System;
using System.Collections.Generic;
public interface IInventory
{
bool MoveItem(long fromPlayer, long toPlayer, long itemUid); // 성공 시 true
bool MoveGold(long fromPlayer, long toPlayer, long amount); // 성공 시 true
}
public class TradeOffer
{
public long PlayerId;
public List<long> ItemUids = new();
public long Gold;
public bool Confirmed;
}
public class TradeSession
{
public long TradeId;
public TradeOffer A;
public TradeOffer B;
public bool Committed;
public bool Cancelled;
}
public class TradeService
{
private readonly Dictionary<long, TradeSession> _trades = new(); // tradeId -> 세션
private readonly Dictionary<long, long> _playerToTrade = new(); // playerId -> tradeId
private readonly IInventory _inv;
public TradeService(IInventory inv) { _inv = inv; }
// 양쪽이 모두 확정하면 commit
public void OnConfirm(long tradeId, long playerId)
{
if (!_trades.TryGetValue(tradeId, out var t)) return;
if (playerId == t.A.PlayerId) t.A.Confirmed = true;
else if (playerId == t.B.PlayerId) t.B.Confirmed = true;
else return;
// (A) commit 진입 가드를 검토하라
if (t.A.Confirmed && t.B.Confirmed && !t.Committed)
Commit(t);
}
private void Commit(TradeSession t)
{
t.Committed = true;
// (B) 이 교환 절차(단계별 이동/반환값)를 검토하라
foreach (var uid in t.A.ItemUids) _inv.MoveItem(t.A.PlayerId, t.B.PlayerId, uid);
foreach (var uid in t.B.ItemUids) _inv.MoveItem(t.B.PlayerId, t.A.PlayerId, uid);
_inv.MoveGold(t.A.PlayerId, t.B.PlayerId, t.A.Gold);
_inv.MoveGold(t.B.PlayerId, t.A.PlayerId, t.B.Gold);
_trades.Remove(t.TradeId);
_playerToTrade.Remove(t.A.PlayerId);
_playerToTrade.Remove(t.B.PlayerId);
}
// 세션 종료(네트워크 콜백, 다른 스레드)
public void OnDisconnect(long playerId)
{
if (!_playerToTrade.TryGetValue(playerId, out var tradeId)) return;
if (!_trades.TryGetValue(tradeId, out var t)) return;
// (C) 이 정리 경로와 commit 의 관계를 검토하라
if (!t.Committed)
{
t.Cancelled = true;
_trades.Remove(tradeId); // 이미 일부 적용됐을 수도 있는데 그냥 제거
_playerToTrade.Remove(t.A.PlayerId);
_playerToTrade.Remove(t.B.PlayerId);
}
}
} 결함 코드 · C++
// ============================================================================
// [코드리뷰 문제] C++ - 거래 도중 한쪽이 로그아웃/접속종료되어 롤백이 필요한 상황
// ----------------------------------------------------------------------------
// 시나리오 (세션/네트워크 · 서버-클라):
// - 두 플레이어가 직거래 창에 각자 아이템/골드를 올리고, 양쪽이 "확정(confirm)" 하면
// 서버가 교환을 commit 한다(A의 물건은 B에게, B의 물건은 A에게).
// - commit 직전/도중에 한쪽 세션이 끊길 수 있다(로그아웃/연결 끊김 -> onDisconnect).
// 이 경우 거래는 안전하게 취소(롤백)되어야 한다.
// - onConfirm 과 onDisconnect 는 서로 다른 스레드(요청 처리/네트워크 콜백)에서 들어온다.
//
// 요구사항:
// - 교환은 원자적이어야 한다(부분 적용으로 인한 아이템 증발/복제 금지).
// - commit 과 disconnect-취소가 동시에 와도 정확히 한 번 commit 되거나 정확히 한 번 취소.
// - 진행 중 거래를 방치(좀비)하거나, 거래 객체를 한쪽이 파괴하는 사이 다른 쪽이 참조하면 안 됨.
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 부분 적용(복제/증발)·이중 처리·좀비 거래·
// 거래 객체 수명 문제·동시 처리 순서 문제가 발생하는지 설명하고, 수정안과 더 나은
// 설계를 제시하라. (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
#include <cstdint>
#include <unordered_map>
#include <vector>
struct Inventory {
virtual bool moveItem(int64_t from, int64_t to, int64_t itemUid) = 0;
virtual bool moveGold(int64_t from, int64_t to, int64_t amount) = 0;
virtual ~Inventory() = default;
};
struct TradeOffer {
int64_t playerId;
std::vector<int64_t> itemUids;
int64_t gold;
bool confirmed = false;
};
struct TradeSession {
int64_t tradeId;
TradeOffer a;
TradeOffer b;
bool committed = false;
bool cancelled = false;
};
class TradeService {
public:
explicit TradeService(Inventory& inv) : inv_(inv) {}
void onConfirm(int64_t tradeId, int64_t playerId) {
auto it = trades_.find(tradeId);
if (it == trades_.end()) return;
TradeSession& t = it->second; // 맵 노드 참조를 들고 진행
if (playerId == t.a.playerId) t.a.confirmed = true;
else if (playerId == t.b.playerId) t.b.confirmed = true;
else return;
// (A) commit 진입 가드를 검토하라
if (t.a.confirmed && t.b.confirmed && !t.committed)
commit(t);
}
void onDisconnect(int64_t playerId) {
auto pit = playerToTrade_.find(playerId);
if (pit == playerToTrade_.end()) return;
auto it = trades_.find(pit->second);
if (it == trades_.end()) return;
TradeSession& t = it->second;
// (C) 이 정리 경로와 commit 의 관계를 검토하라
if (!t.committed) {
t.cancelled = true;
playerToTrade_.erase(t.a.playerId);
playerToTrade_.erase(t.b.playerId);
trades_.erase(it); // 이 시점의 t 사용 관계를 검토하라
}
}
private:
void commit(TradeSession& t) {
t.committed = true;
// (B) 이 교환 절차(단계별 이동/반환값)를 검토하라
for (auto uid : t.a.itemUids) inv_.moveItem(t.a.playerId, t.b.playerId, uid);
for (auto uid : t.b.itemUids) inv_.moveItem(t.b.playerId, t.a.playerId, uid);
inv_.moveGold(t.a.playerId, t.b.playerId, t.a.gold);
inv_.moveGold(t.b.playerId, t.a.playerId, t.b.gold);
playerToTrade_.erase(t.a.playerId);
playerToTrade_.erase(t.b.playerId);
trades_.erase(t.tradeId); // 이 제거 이후 t 사용 관계를 검토하라
}
Inventory& inv_;
std::unordered_map<int64_t, TradeSession> trades_; // tradeId -> 세션
std::unordered_map<int64_t, int64_t> playerToTrade_;
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.