13. Split-brain: 두 존 서버가 같은 플레이어를 동시에 활성화 — C#
난이도 최상 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - Split-brain: 두 존 서버가 같은 플레이어를 동시에 활성화
// ----------------------------------------------------------------------------
// 시나리오 (서버-서버 / 분산):
// - 심리스 월드에서 플레이어가 존 경계를 넘으면, 소유권이 존 서버 A → B 로
// 핸드오프된다. 이때 B 가 플레이어를 "활성화(TakeOver)" 한다.
// - 플레이어의 모든 권위 있는 변경(골드/아이템/위치)은 "현재 소유 서버" 만
// 적용할 수 있어야 한다. 동시에 두 서버가 적용하면 안 된다(split-brain).
// - 핸드오프 순간, A 에는 아직 처리 중이던(in-flight) 명령이 남아 있을 수 있고,
// 네트워크 지연/재정렬로 활성화 메시지가 늦거나 뒤바뀔 수 있다.
// - OwnershipRegistry 는 여러 서버가 공유하는 권위 저장소(분산 KV/공유DB 추상화)다.
//
// 요구사항:
// - 어떤 순간에도 플레이어를 적용 가능한 서버는 정확히 하나여야 한다.
// - 핸드오프 이후 옛 소유 서버(A)의 늦은 명령은 거부되어야 한다(펜싱).
// - 활성화/소유권 이전은 원자적이고, 재정렬된 늦은 이전은 무시되어야 한다.
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 어떻게 두 서버가 동시에 같은 플레이어에
// 쓰기를 하게 되는지(인터리빙/재정렬 포함) 설명하고, 수정안과 더 나은 설계를
// 제시하라. (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
using System.Collections.Generic;
public struct Command
{
public long GoldDelta;
public int AddItemId;
}
public class PlayerState
{
public long Gold;
// 인벤토리 등 생략
}
// 여러 서버가 공유하는 권위 소유권 저장소(분산 KV/공유DB 추상화).
public class OwnershipRegistry
{
private readonly Dictionary<long, int> _owner = new Dictionary<long, int>(); // playerId -> serverId
// 이 서버가 플레이어를 넘겨받아 소유자로 등록한다.
public void Activate(long playerId, int serverId)
{
_owner[playerId] = serverId; // (A) 현재 소유 서버를 덮어쓴다
}
public int OwnerOf(long playerId)
{
return _owner.TryGetValue(playerId, out int s) ? s : -1;
}
}
// 각 존 서버.
public class ZoneServer
{
private readonly int _myId;
private readonly OwnershipRegistry _reg;
private readonly Dictionary<long, int> _cachedOwner = new Dictionary<long, int>();
public ZoneServer(int myId, OwnershipRegistry reg)
{
_myId = myId;
_reg = reg;
}
// 핸드오프: 이 서버가 플레이어를 넘겨받아 활성화한다.
public void TakeOver(long playerId)
{
_reg.Activate(playerId, _myId);
_cachedOwner[playerId] = _myId; // 빠른 판정을 위해 로컬 캐시
}
// 클라이언트/게이트웨이에서 온 권위 명령을 적용한다.
public bool ApplyCommand(long playerId, Command c, PlayerState st)
{
// (B) 내가 소유자인지 로컬 캐시로 판정
if (_cachedOwner[playerId] != _myId)
return false;
// (C) 적용
st.Gold += c.GoldDelta;
// 아이템 추가 등 생략
return true;
}
} 결함 코드 · C++
// ============================================================================
// [코드리뷰 문제] C++ - Split-brain: 두 존 서버가 같은 플레이어를 동시에 활성화
// ----------------------------------------------------------------------------
// 시나리오 (서버-서버 / 분산):
// - 심리스 월드에서 플레이어가 존 경계를 넘으면, 소유권이 존 서버 A → B 로
// 핸드오프된다. 이때 B 가 플레이어를 "활성화(TakeOver)" 한다.
// - 플레이어의 모든 권위 있는 변경(골드/아이템/위치)은 "현재 소유 서버" 만
// 적용할 수 있어야 한다. 동시에 두 서버가 적용하면 안 된다(split-brain).
// - 핸드오프 순간, A 에는 아직 처리 중이던(in-flight) 명령이 남아 있을 수 있고,
// 네트워크 지연/재정렬로 활성화 메시지가 늦거나 뒤바뀔 수 있다.
// - OwnershipRegistry 는 여러 서버가 공유하는 권위 저장소(분산 KV/공유DB 추상화)다.
//
// 요구사항:
// - 어떤 순간에도 플레이어를 적용 가능한 서버는 정확히 하나여야 한다.
// - 핸드오프 이후 옛 소유 서버(A)의 늦은 명령은 거부되어야 한다(펜싱).
// - 활성화/소유권 이전은 원자적이고, 재정렬된 늦은 이전은 무시되어야 한다.
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 어떻게 두 서버가 동시에 같은 플레이어에
// 쓰기를 하게 되는지(인터리빙/재정렬 포함) 설명하고, 수정안과 더 나은 설계를
// 제시하라. (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
#include <cstdint>
#include <unordered_map>
struct Command {
int64_t goldDelta = 0;
int addItemId = 0;
};
struct PlayerState {
int64_t gold = 0;
// 인벤토리 등 생략
};
// 여러 서버가 공유하는 권위 소유권 저장소(분산 KV/공유DB 추상화).
class OwnershipRegistry {
public:
// 이 서버가 플레이어를 넘겨받아 소유자로 등록한다.
void Activate(int64_t playerId, int serverId)
{
owner_[playerId] = serverId; // (A) 현재 소유 서버를 덮어쓴다
}
int OwnerOf(int64_t playerId)
{
return owner_.count(playerId) ? owner_[playerId] : -1;
}
private:
std::unordered_map<int64_t, int> owner_; // playerId -> serverId
};
// 각 존 서버.
class ZoneServer {
public:
ZoneServer(int myId, OwnershipRegistry& reg) : myId_(myId), reg_(reg) {}
// 핸드오프: 이 서버가 플레이어를 넘겨받아 활성화한다.
void TakeOver(int64_t playerId)
{
reg_.Activate(playerId, myId_);
cachedOwner_[playerId] = myId_; // 빠른 판정을 위해 로컬 캐시
}
// 클라이언트/게이트웨이에서 온 권위 명령을 적용한다.
bool ApplyCommand(int64_t playerId, const Command& c, PlayerState& st)
{
// (B) 내가 소유자인지 로컬 캐시로 판정
if (cachedOwner_[playerId] != myId_)
return false;
// (C) 적용
st.gold += c.goldDelta;
// 아이템 추가 등 생략
return true;
}
private:
int myId_;
OwnershipRegistry& reg_;
std::unordered_map<int64_t, int> cachedOwner_;
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.