17. 관심영역(AoI) 진입/이탈 시 엔티티 동기화 경합

난이도 중 해설 보기 →

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

결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 관심영역(AoI) 진입/이탈 시 엔티티 동기화 경합
// ----------------------------------------------------------------------------
// 시나리오:
//   - 심리스 월드에서 각 관찰자(플레이어)는 자기 주변 일정 반경(AoI)의 엔티티만
//     본다. 서버는 관찰자별로 "현재 보이는 엔티티 집합(_visible)"을 관리한다.
//   - 주변 엔티티가 움직이면 이동 워커 스레드가 Update(near) 를 호출해, 새로 시야에
//     들어온 엔티티에는 Spawn 패킷을, 시야를 벗어난 엔티티에는 Despawn 패킷을 보낸다.
//   - 동시에 다른 스레드는 엔티티 접속종료/사망 시 OnEntityRemoved 를 호출해 시야에서
//     제거한다. 여러 엔티티가 동시에 움직이므로 같은 관찰자의 뷰가 동시 갱신될 수 있다.
//
// 요구사항:
//   - 이미 보이는 엔티티에 Spawn 을 두 번 보내면 안 되고, 안 보이는 엔티티에 Despawn 을
//     중복으로 보내면 안 된다(Spawn/Despawn 짝 맞춤).
//   - 시야에서 사라진(또는 제거된) 엔티티가 계속 보이는 "유령" 이 없어야 한다.
//   - 동시 갱신에도 집합 자료구조가 손상되거나 정의되지 않은 동작이 나면 안 된다.
//
// 과제:
//   이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 깨지는지(동시 인터리빙 포함)
//   설명하고, 수정안과 더 나은 설계를 제시하라.
//   (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================

using System;
using System.Collections.Generic;

public class Entity
{
    public int   Id;
    public float X, Y;
    public bool  Alive = true;
}

public interface IClient
{
    void SendSpawn(Entity e);
    void SendDespawn(int entityId);
}

public class AoIView
{
    // 이 관찰자가 현재 보고 있는 엔티티 ID 집합
    private readonly HashSet<int> _visible = new();
    private readonly IClient _client;

    public AoIView(IClient client) { _client = client; }

    // 이동 워커가 주변 후보 목록(near)으로 시야를 갱신한다.
    public void Update(IReadOnlyList<Entity> near)
    {
        // 1) 새로 보이는 것 Spawn
        foreach (var e in near)
        {
            // (A)
            if (!_visible.Contains(e.Id))
            {
                _visible.Add(e.Id);
                _client.SendSpawn(e);
            }
        }

        // 2) 더 이상 안 보이는 것 Despawn
        // (B)
        foreach (var id in _visible)
        {
            if (!ContainsId(near, id))
            {
                _visible.Remove(id);          // 순회 중 컬렉션 수정
                _client.SendDespawn(id);
            }
        }
    }

    // 엔티티가 월드에서 제거(접속종료/사망)될 때 호출
    public void OnEntityRemoved(Entity e)
    {
        // (C)
        if (_visible.Contains(e.Id))
        {
            _visible.Remove(e.Id);
            _client.SendDespawn(e.Id);
        }
    }

    private static bool ContainsId(IReadOnlyList<Entity> near, int id)
    {
        foreach (var e in near)
            if (e.Id == id) return true;
        return false;
    }
}
내 리뷰 · C#
내 답안 · 자동 저장

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