5. C# 송신 큐 백프레셔 + 끊김/재접속 세션 복구 (복합)

난이도 최상 해설 보기 →

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

결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 송신 큐 백프레셔 + 끊김/재접속 세션 복구 (복합)
// ----------------------------------------------------------------------------
// 시나리오 (실서비스 MMO 게이트웨이):
//   - 각 세션은 송신 큐를 두고, 단일 비동기 송신 루프가 큐를 비운다.
//     (소켓에 동시에 둘 이상 WriteAsync 하면 안 되므로 직렬화한다.)
//   - 일부 클라는 모바일/해외라 느리다(slow consumer). 서버 브로드캐스트는
//     초당 수십~수백 패킷을 모든 세션에 Enqueue 한다.
//   - 연결이 끊기면 세션을 즉시 버리지 않고 ResumeWindow(예: 30초) 동안
//     보관한다. 같은 토큰으로 재접속하면 기존 세션 상태와 "보내다 만" 패킷을
//     이어서 보낸다(seamless reconnect).
//   - Enqueue 는 게임 로직 스레드들에서, 송신 루프는 IO, 끊김/재접속은 또 다른
//     스레드에서 동시에 일어난다.
//
// 요구사항:
//   - 느린 클라 하나가 서버 전체 메모리를 끌어내리면 안 된다(백프레셔/흐름 제어).
//   - 재접속 시 패킷 유실/중복/순서 뒤바뀜이 없어야 한다(정합성).
//   - 끊김 중 쌓인 큐가 무한히 자라거나 영원히 남으면 안 된다.
//
// 과제:
//   이 코드의 잠재적 문제를 모두 찾고, 왜 문제인지 설명하고,
//   수정안과 더 나은 설계를 제시하라.
//   (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================

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

public sealed class GameSession
{
    public string ResumeToken;
    private Socket _socket;
    private readonly Queue<byte[]> _sendQueue = new Queue<byte[]>();   // (A)
    private bool _sending = false;                                     // (B)
    public DateTime DisconnectedAt;
    public bool Connected = true;

    public GameSession(Socket s) { _socket = s; }

    // 게임 로직 스레드들에서 호출 (브로드캐스트 등)
    public void Enqueue(byte[] packet)
    {
        lock (_sendQueue)
        {
            _sendQueue.Enqueue(packet);   // (A)
            if (!_sending)
            {
                _sending = true;
                _ = SendLoopAsync();      // (B)
            }
        }
    }

    private async Task SendLoopAsync()
    {
        while (true)
        {
            byte[] next;
            lock (_sendQueue)
            {
                if (_sendQueue.Count == 0) { _sending = false; return; }
                next = _sendQueue.Dequeue();
            }
            // (C) 느린 클라면 여기서 오래 블록될 수 있음
            await _socket.SendAsync(next, SocketFlags.None);
        }
    }

    // 끊김 시 호출
    public void OnDisconnect()
    {
        Connected = false;
        DisconnectedAt = DateTime.UtcNow;
        // 큐는 그대로 둔다(재접속 시 이어 보내려고). (D)
    }

    // 재접속 시 호출: 새 소켓으로 교체하고 큐를 이어서 보낸다
    public void Resume(Socket newSocket)
    {
        _socket = newSocket;              // (E) 소켓 교체
        Connected = true;
        if (!_sending)
        {
            _sending = true;
            _ = SendLoopAsync();          // 남은 큐 이어 보내기
        }
    }
}

public sealed class SessionRegistry
{
    private readonly Dictionary<string, GameSession> _byToken =
        new Dictionary<string, GameSession>();

    public GameSession FindForResume(string token)
    {
        // (F) 만료(ResumeWindow) 검사/정리 로직이 없다
        _byToken.TryGetValue(token, out var s);
        return s;
    }

    public void Register(GameSession s) => _byToken[s.ResumeToken] = s;
}
내 리뷰 · C#
내 답안 · 자동 저장

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