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

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