10. C# 서버 간 세션 핸드오프/마이그레이션 + Graceful Shutdown 드레이닝 (복합)

난이도 최상 해설 보기 →

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

결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 서버 간 세션 핸드오프/마이그레이션 + Graceful Shutdown 드레이닝 (복합)
// ----------------------------------------------------------------------------
// 시나리오 (배포/오토스케일 다운 시 무중단 마이그레이션):
//   - 게이트웨이 노드 풀. 배포·스케일인 시 특정 노드를 "드레이닝(draining)"으로
//     전환하고, 그 노드의 활성 세션을 다른 노드로 핸드오프한 뒤 종료한다.
//   - 핸드오프: (1) 대상 노드에 세션 상태(스냅샷)를 전송 → (2) 클라에 "새 노드로
//     재접속하라" 지시(migrate ticket 발급) → (3) 클라가 새 노드에 재접속 →
//     (4) 옛 노드에서 세션 제거.
//   - Drain 시작 후에도 새 접속 시도, in-flight 요청, 진행 중 핸드오프가 섞인다.
//   - 여러 스레드(수신/타이머/드레인 트리거)가 동시에 세션 테이블을 만진다.
//
// 요구사항:
//   - 드레이닝 중 진행 중 작업은 끝까지 처리(graceful), 새 접속은 막거나 리다이렉트.
//   - 핸드오프 중 메시지 유실/중복/순서 뒤바뀜 없이 한 노드에서만 활성이어야 한다
//     (split-brain 금지: 두 노드가 동시에 같은 세션을 활성으로 보지 않기).
//   - 옛 노드는 모든 세션이 비워진 뒤에만 종료한다(강제 종료로 유저 끊김 금지).
//
// 과제:
//   이 코드의 잠재적 문제를 모두 찾고, 왜 문제인지 설명하고,
//   수정안과 더 나은 설계를 제시하라.
//   (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

public sealed class Session
{
    public long Id;
    public bool Active = true;          // (A)
    public byte[] Snapshot() => Array.Empty<byte>();   // 상태 직렬화(생략)
    public void RedirectTo(string node) { /* 클라에 migrate ticket 전송 */ }
    public void Close() { /* 소켓 종료 */ }
}

public sealed class GatewayNode
{
    private readonly Dictionary<long, Session> _sessions = new Dictionary<long, Session>();
    private readonly object _lock = new object();
    public volatile bool Draining = false;     // (B)

    // 신규 접속 처리 (수신 스레드)
    public Session OnNewConnection(long sessionId)
    {
        if (Draining) return null;             // (C) 드레이닝이면 거절

        var s = new Session { Id = sessionId };
        lock (_lock) { _sessions[sessionId] = s; }
        return s;
    }

    // 드레인 시작: 모든 세션을 targetNode 로 핸드오프하고 종료 준비
    public async Task DrainAndHandoffAsync(string targetNode, IPeer peer)
    {
        Draining = true;                       // (B)

        List<Session> snapshot;
        lock (_lock) { snapshot = new List<Session>(_sessions.Values); }   // (D)

        foreach (var s in snapshot)
        {
            // 1) 대상 노드로 상태 전송
            await peer.SendSnapshotAsync(targetNode, s.Id, s.Snapshot());   // (E)

            // 2) 클라에 새 노드로 재접속 지시
            s.RedirectTo(targetNode);          // (F)

            // 3) 옛 노드에서 제거
            lock (_lock) { _sessions.Remove(s.Id); }
            s.Close();                          // (G) 즉시 닫음
        }

        // 모든 세션 처리 끝 → 프로세스 종료
        Environment.Exit(0);                    // (H)
    }

    // in-flight 게임 요청 처리 (수신 스레드)
    public void HandleRequest(long sessionId, byte[] req)
    {
        Session s;
        lock (_lock) { _sessions.TryGetValue(sessionId, out s); }
        if (s == null) return;
        // ... 요청 처리 (드레이닝/핸드오프 진행 여부와 무관하게 그냥 처리) (I)
    }
}

public interface IPeer
{
    Task SendSnapshotAsync(string node, long sessionId, byte[] snapshot);
}
내 리뷰 · C#
내 답안 · 자동 저장

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