2. C# 룸 기반 게임의 좀비 세션 / 유령 플레이어

난이도 중 해설 보기 →

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

결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 룸 기반 게임에서 좀비 세션 / 유령 플레이어
// ----------------------------------------------------------------------------
// 시나리오:
//   - 플레이어는 접속 후 Room 에 입장(Enter)해 함께 플레이한다.
//   - 연결이 끊기면(소켓 에러/정상 종료) OnDisconnect 가 호출된다.
//   - SessionManager 는 sessionId -> Session 맵을 들고 전체 세션을 관리한다.
//   - Room 은 자신에 속한 세션들을 들고 있고, 매 틱 브로드캐스트한다.
//   - IO 스레드 풀(N개)에서 OnDisconnect 가, 게임 로직 스레드에서 Room.Tick 이
//     동시에 돈다.
//
// 요구사항:
//   - 연결이 끊긴 플레이어는 즉시 Room/매니저에서 사라져야 한다(유령 금지).
//   - 끊긴 세션의 자원/구독은 회수되어야 한다(누수 금지).
//
// 과제:
//   이 코드의 잠재적 문제를 모두 찾고, 왜 문제인지 설명하고,
//   수정안과 더 나은 설계를 제시하라.
//   (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================

using System;
using System.Collections.Generic;

public sealed class Session
{
    public long Id;
    public bool Connected = true;
    public Room Room;        // (A) 자신이 속한 룸을 강한 참조로 보유

    public void Send(byte[] data) { /* 비동기 송신 ... */ }
}

public sealed class Room
{
    private readonly object _lock = new object();
    private readonly List<Session> _members = new List<Session>();   // (A) 멤버 세션 강한 참조

    // 룸이 매 틱 만든 스냅샷을 멤버에게 전달하는 이벤트.
    // 세션은 입장 시 여기에 핸들러를 등록한다. (B)
    public event Action<byte[]> OnBroadcast;

    public void Enter(Session s)
    {
        lock (_lock)
        {
            _members.Add(s);
            s.Room = this;
            // 세션의 Send 를 브로드캐스트 이벤트에 구독시킨다.
            OnBroadcast += s.Send;   // (B) 이벤트 핸들러 등록
        }
    }

    // (C) 게임 로직 스레드: 매 틱 호출
    public void Tick(byte[] snapshot)
    {
        lock (_lock)
        {
            // 끊긴 세션에도 그대로 보낸다
            OnBroadcast?.Invoke(snapshot);   // 구독된 모든 Send 호출(락 보유 중)
        }
    }
}

public sealed class SessionManager
{
    private readonly object _lock = new object();
    private readonly Dictionary<long, Session> _sessions = new Dictionary<long, Session>();

    public void Add(Session s)
    {
        lock (_lock) { _sessions[s.Id] = s; }
    }

    // (D) IO 스레드 풀에서 연결 종료 시 호출
    public void OnDisconnect(long id)
    {
        lock (_lock)
        {
            if (!_sessions.TryGetValue(id, out var s)) return;
            s.Connected = false;
            // 매니저 맵에서만 지운다. Room 에서는 빼지 않는다. (E)
            _sessions.Remove(id);
        }
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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