19. 채널/인스턴스 이동 중 상태 이관 누락

난이도 중 해설 보기 →

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

결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 채널/인스턴스 이동 중 상태 이관 누락
// ----------------------------------------------------------------------------
// 시나리오 (세션/네트워크 · 서버-서버):
//   - 플레이어가 채널/인스턴스(예: 마을 채널 1 -> 채널 2, 또는 던전 인스턴스)로 이동한다.
//   - 이동은 "출발 서버(source)" 가 플레이어 상태를 직렬화해 "대상 서버(target)" 로 넘기는
//     핸드오프로 처리된다. 대상 서버가 상태를 받아 플레이어를 활성화하면 이동 완료.
//   - 플레이어의 휘발 상태에는 버프(남은 지속시간), 스킬 쿨다운, 진행 중 시전(cast),
//     수령 대기 전리품 등이 있다. 이동 후에도 이 상태는 그대로 유지돼야 한다.
//   - 핸드오프 도중에도 출발 서버에는 이 플레이어를 향한 패킷(데미지/버프/채팅)이 계속 들어온다.
//
// 요구사항:
//   - 휘발 상태(버프 남은시간·쿨다운·진행 중 시전 등)가 이동 후 유실되면 안 된다.
//   - 어느 순간에도 같은 플레이어가 두 서버에서 동시에 활성(중복 처리)되면 안 되고,
//     반대로 양쪽 모두 비활성(패킷 블랙홀)인 창도 최소화해야 한다.
//   - 핸드오프 중 출발 서버에 들어온 갱신이 사라지면 안 된다.
//
// 과제:
//   이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 상태가 유실되거나 중복/블랙홀 활성이
//   생기는지 설명하고, 수정안과 더 나은 설계를 제시하라.
//   (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================

using System;
using System.Collections.Generic;

public class Buff
{
    public int  BuffId;
    public long ExpireAtMs;   // 만료 절대시각
}

public class PlayerState
{
    public long          PlayerId;
    public int           Hp;
    public List<Buff>    Buffs = new();
    public Dictionary<int, long> CooldownUntilMs = new();   // skillId -> 쿨다운 종료시각
    // 진행 중 시전(있으면 비어있지 않음)
    public int           CastingSkillId;     // 0 = 없음
}

// 직렬화돼 대상 서버로 전송되는 이관 페이로드
public class MigrationPayload
{
    public long PlayerId;
    public int  Hp;
    public List<Buff> Buffs;
}

public class MigrationService
{
    private readonly Dictionary<long, PlayerState> _active = new();   // 이 서버에서 활성 플레이어
    private readonly Func<string, MigrationPayload, bool> _sendToTarget;  // (targetServer, payload)

    public MigrationService(Func<string, MigrationPayload, bool> sendToTarget)
    {
        _sendToTarget = sendToTarget;
    }

    // 출발 서버: 플레이어를 대상 서버로 이관 시작
    public bool BeginMigrate(long playerId, string targetServer)
    {
        if (!_active.TryGetValue(playerId, out var st)) return false;

        // (A) 휘발 상태 중 일부만 직렬화 (Hp, Buffs 만)
        var payload = new MigrationPayload
        {
            PlayerId = st.PlayerId,
            Hp       = st.Hp,
            Buffs    = st.Buffs,
        };

        // (B) 대상으로 보내기 전에 출발 서버에서 먼저 세션 제거
        _active.Remove(playerId);

        // (C) 전송 (성공/실패 무시)
        _sendToTarget(targetServer, payload);
        return true;
    }

    // 출발 서버: 핸드오프 도중에도 들어오는 플레이어 갱신
    public void ApplyUpdate(long playerId, Action<PlayerState> mutate)
    {
        if (!_active.TryGetValue(playerId, out var st)) return;   // 없으면 그냥 무시
        mutate(st);
    }

    // 대상 서버: 이관 페이로드 수신 -> 활성화
    public void OnReceiveMigration(MigrationPayload payload)
    {
        var st = new PlayerState
        {
            PlayerId = payload.PlayerId,
            Hp       = payload.Hp,
            Buffs    = payload.Buffs ?? new List<Buff>(),
            // CooldownUntilMs, CastingSkillId 는 페이로드에 없어 초기화됨
        };
        _active[payload.PlayerId] = st;
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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