45. 지연 삭제 큐의 중복 등록과 풀 이중 반납 (C#)

난이도 중 해설 보기 →

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

결함 코드 · C#
// [시나리오]
// 존(zone) 서버의 틱 루프. 엔티티를 틱 도중에 즉시 파괴하면 순회 중인 시스템들이 깨지므로,
// 삭제 요청을 큐에 모아 두었다가 틱 끝에서 한꺼번에 처리하는 "지연 삭제(deferred delete)"를
// 쓴다. 엔티티 객체는 GC 압력을 줄이기 위해 오브젝트 풀에서 대여/반납한다.
//
// 삭제 요청은 여러 경로에서 들어올 수 있다.
//   - 체력이 0이 되어 사망 처리
//   - 지속시간이 끝난 소환수/투사체의 만료
//   - 플레이어 접속 종료
//   - 존 이동으로 인한 디스폰
// 이 경로들은 서로를 알지 못하며, 같은 틱에 같은 엔티티에 대해 각각 삭제를 요청할 수 있다.
// 일부 경로는 틱 스레드가 아닌 워커 스레드(네트워크 I/O 완료 등)에서 호출된다.
//
// [요구사항]
// - 한 엔티티는 정확히 한 번만 파괴되고, 정확히 한 번만 풀에 반납된다.
// - 파괴 이벤트(OnEntityDestroyed)는 엔티티당 1회만 발행된다.
// - 삭제 예약된 엔티티는 다음 틱부터 조회되지 않는다.
//
// [과제]
// (A), (B) 지점을 중심으로 요구사항이 깨지는 조건을 찾고, 재현 시나리오와 수정안을
// 제시하시오.

using System;
using System.Collections.Generic;

namespace GameServer.Zone
{
    public sealed class Entity
    {
        public int Id;
        public bool Alive = true;
        public object State;

        public void Reset() { Id = 0; Alive = true; State = null; }
    }

    public sealed class EntityPool
    {
        private readonly Stack<Entity> _free = new();
        public Entity Rent() => _free.Count > 0 ? _free.Pop() : new Entity();
        public void Return(Entity e) { e.Reset(); _free.Push(e); }
    }

    public sealed class ZoneWorld
    {
        private readonly Dictionary<int, Entity> _entities = new();
        private readonly List<Entity> _pendingDelete = new();
        private readonly EntityPool _pool = new();

        public event Action<int> OnEntityDestroyed;

        public Entity Find(int id) => _entities.TryGetValue(id, out var e) ? e : null;

        // 사망/만료/접속종료/디스폰 등 여러 경로에서 호출된다.
        public void RequestDelete(int id)
        {
            var e = Find(id);
            if (e == null) return;

            // (A)
            _pendingDelete.Add(e);
        }

        // 틱 끝에서 호출된다.
        public void FlushDeletions()
        {
            // (B)
            foreach (var e in _pendingDelete)
            {
                e.Alive = false;
                _entities.Remove(e.Id);
                OnEntityDestroyed?.Invoke(e.Id);
                _pool.Return(e);
            }
            _pendingDelete.Clear();
        }
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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