45. 존 경계 핸드오프의 비원자 이동과 소속 붕괴 (C#)
난이도 중 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// 시나리오: 심리스(seamless) 오픈월드 MMORPG. 월드는 여러 개의 존(Zone)으로 나뉘고,
// 각 존은 자신의 관리 리스트에 소속 플레이어를 담아 AoI(관심영역) 브로드캐스트를 돌린다.
// 플레이어가 존 경계를 넘으면 이전 존에서 빠지고 다음 존으로 들어가는 "핸드오프"가 일어난다.
//
// 요구사항:
// - 핸드오프가 끝난 시점에 플레이어는 정확히 하나의 존에만 소속되어야 한다.
// - 이동 처리와 브로드캐스트는 서로 다른 스레드(이동 워커 / 존 틱 워커)에서 동시에 돈다.
// - 경계를 빠르게 왕복하거나 여러 이동 패킷이 몰려도 소속 상태가 깨지지 않아야 한다.
//
// 과제: MovePlayer 와 관련 코드를 검토하고 동시성 관점에서 무엇이 잘못될 수 있는지,
// 어떤 조건에서 재현되는지, 어떻게 고칠지 설명하라. (A)(B) 지점을 특히 살펴보라.
using System.Collections.Generic;
public sealed class Zone
{
public int Id;
public readonly List<Player> Members = new List<Player>();
public void Add(Player p) { Members.Add(p); }
public void Remove(Player p) { Members.Remove(p); }
// 존 틱 워커 스레드에서 주기적으로 호출된다.
public void BroadcastTick()
{
foreach (var p in Members) // (B)
p.SendAoISnapshot(Members);
}
}
public sealed class Player
{
public int Id;
public Zone CurrentZone;
public void SendAoISnapshot(List<Player> around) { /* 직렬화·송신 */ }
}
public sealed class World
{
private readonly Dictionary<(int, int), Zone> _grid = new Dictionary<(int, int), Zone>();
public Zone ZoneAt(float x, float y) => _grid[((int)x / 128, (int)y / 128)];
// 이동 워커 스레드에서 플레이어의 위치 패킷마다 호출된다.
public void MovePlayer(Player p, float x, float y)
{
Zone from = p.CurrentZone;
Zone to = ZoneAt(x, y);
if (from == to) return;
from.Remove(p); // (A)
to.Add(p);
p.CurrentZone = to;
}
} 결함 코드 · C++
// 시나리오: 심리스 오픈월드 MMORPG. 월드는 여러 Zone 으로 나뉘고, 각 Zone 은
// 소속 플레이어 포인터를 벡터에 담아 AoI 브로드캐스트를 돈다. 플레이어가 경계를
// 넘으면 이전 Zone 에서 빠지고 다음 Zone 으로 들어가는 "핸드오프"가 일어난다.
//
// 요구사항:
// - 핸드오프 후 플레이어는 정확히 하나의 Zone 에만 소속된다.
// - 이동 처리(moveWorker)와 브로드캐스트(tickWorker)는 서로 다른 스레드에서 동시에 돈다.
// - 로그아웃 시 Player 객체는 파괴된다. 어떤 Zone 도 파괴된 Player 를 참조하면 안 된다.
//
// 과제: movePlayer 와 관련 코드를 검토하고 동시성/수명 관점에서 무엇이 잘못될 수 있는지,
// 어떤 조건에서 재현되는지, 어떻게 고칠지 설명하라. (A)(B) 지점을 특히 살펴보라.
#include <vector>
#include <algorithm>
#include <cstdint>
struct Player {
int id{};
struct Zone* currentZone{};
void sendAoISnapshot(const std::vector<Player*>& around);
};
struct Zone {
int id{};
std::vector<Player*> members;
void add(Player* p) { members.push_back(p); }
void remove(Player* p) {
members.erase(std::remove(members.begin(), members.end(), p), members.end());
}
// tickWorker 스레드에서 주기적으로 호출된다.
void broadcastTick() {
for (Player* p : members) // (B)
p->sendAoISnapshot(members);
}
};
struct World {
Zone* zoneAt(float x, float y);
// moveWorker 스레드에서 위치 패킷마다 호출된다.
void movePlayer(Player* p, float x, float y) {
Zone* from = p->currentZone;
Zone* to = zoneAt(x, y);
if (from == to) return;
from->remove(p); // (A)
to->add(p);
p->currentZone = to;
}
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.