37. 인접 테이블의 락 없는 순회와 손상된 열거자 (읽기는 안전하다는 착각)

난이도 중 해설 보기 →

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

결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 존 인접 리스트(broadcast 대상) 캐시
// ----------------------------------------------------------------------------
// 시나리오 (서버 내부):
//   - 존 서버는 플레이어가 반경 안에 들어오고 나갈 때마다 인접 테이블
//     (Dictionary<long, PlayerHandle>)에 삽입/제거한다(이동 스레드, 틱마다 호출).
//   - 브로드캐스트 스레드는 주기적으로 인접 테이블 전체를 훑어 스냅샷 패킷을
//     각 플레이어에게 보낸다.
//   - 삽입/제거(Enter/Leave)는 반드시 락으로 보호하지만, 브로드캐스트는
//     "값을 바꾸는 게 아니라 읽기만 하니 락이 필요 없다"고 보고 락 없이
//     그냥 foreach 로 순회한다.
//   - 동접이 늘면서 이동이 잦아졌고, 브로드캐스트 도중 서버가 예외로
//     죽거나 스킵되는 로그가 간헐적으로 보고된다.
//
// 과제:
//   Enter/Leave(A)와 Broadcast(B)를 각각 검토하라. 두 스레드가 동시에
//   실행될 때 (B)에서 실제로 무슨 일이 벌어질 수 있는지, 그리고 이 문제가
//   "운이 나쁘면 가끔" 발생하는 이유를 설명하라. 수정안을 제시하라.
//   (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================

using System.Collections.Generic;

namespace GameServer.Zone
{
    public sealed class PlayerHandle
    {
        public long Id;
        public float X, Y;
    }

    public sealed class NeighborTable
    {
        private readonly object _lock = new object();
        private readonly Dictionary<long, PlayerHandle> _neighbors = new Dictionary<long, PlayerHandle>();

        // 이동/입퇴장 스레드에서 호출
        public void Enter(PlayerHandle p)
        {
            lock (_lock)                                  // (A) 삽입은 락으로 보호
            {
                _neighbors[p.Id] = p;
            }
        }

        public void Leave(long id)
        {
            lock (_lock)                                  // (A) 제거도 락으로 보호
            {
                _neighbors.Remove(id);
            }
        }

        // 브로드캐스트 스레드에서 주기적으로(예: 100ms) 호출
        public void BroadcastSnapshot()
        {
            // 조회뿐이니 락 없이 순회해도 안전하다고 가정
            foreach (var kv in _neighbors)                // (B)
            {
                PlayerHandle p = kv.Value;
                SendSnapshotPacket(p);
            }
        }

        private void SendSnapshotPacket(PlayerHandle p)
        {
            // 네트워크 전송 (생략)
        }
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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