11. 순서보장 채널 + 비순서 채널 혼용 시 상태 정합성 (C#)
난이도 최상 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 순서보장 채널 + 비순서 채널 혼용 시 상태 정합성
// ----------------------------------------------------------------------------
// 시나리오:
// - 게임은 두 종류의 채널을 함께 쓴다.
// * 신뢰·순서보장 채널(TCP류): 스폰/디스폰, 장비 변경, 사망 등 "중요 이벤트".
// * 비신뢰·비순서 채널(UDP류): 좌표 스냅샷(PosUpdate)처럼 고빈도·유실 허용.
// 두 채널은 서로 독립적이라, 채널 간 도착 순서는 보장되지 않는다(같은 채널
// 안에서도 비순서 채널은 재정렬/유실/중복 가능).
// - 각 패킷 와이어: [ushort seq][byte type][payload], seq 는 채널별로 1씩 증가.
// - 클라이언트는 수신한 패킷을 엔티티 뷰(_entities)에 반영한다.
// - 서버는 좌표를 비신뢰 채널로, 스폰/디스폰/사망을 신뢰 채널로 보낸다.
//
// 요구사항:
// - 늦게/뒤늦게 도착한 옛 좌표 스냅샷이 최신 좌표를 덮어쓰면 안 된다.
// - 채널 간 인과관계(스폰 → 그 엔티티의 좌표)가 깨져도 유령/누락 엔티티가
// 생기면 안 된다.
// - 사망처럼 한 번 놓치면 상태가 영구히 어긋나는 이벤트는 유실되면 안 된다.
// - seq 는 ushort 라 65536 마다 한 바퀴 돈다(wrap).
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 정합성이 깨지는지 설명하고,
// 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
using System.Collections.Generic;
public enum MsgType : byte { Spawn = 1, Despawn = 2, Pos = 3, Death = 4 }
public struct Payload
{
public uint EntityId;
public float X, Y;
}
public class EntityView
{
public uint Id;
public float X, Y;
public bool Alive;
}
public class ClientWorld
{
private readonly Dictionary<uint, EntityView> _entities = new();
private ushort _lastSeq = 0; // (E) 모든 엔티티/타입 공통 last seq
// 신뢰·순서보장 채널 수신 (TCP류) — 채널 내 순서는 보장됨
public void OnReliable(ushort seq, MsgType type, Payload p)
{
switch (type)
{
case MsgType.Spawn:
_entities[p.EntityId] = new EntityView
{ Id = p.EntityId, X = p.X, Y = p.Y, Alive = true };
break;
case MsgType.Despawn:
_entities.Remove(p.EntityId);
break;
}
}
// 비신뢰·비순서 채널 수신 (UDP류) — 재정렬/유실/중복 가능
public void OnUnreliable(ushort seq, MsgType type, Payload p)
{
switch (type)
{
case MsgType.Pos:
// (A) 전역 last seq 로만 최신 여부 판단
if (seq <= _lastSeq) // (B) wrap 미고려 비교
return;
_lastSeq = seq;
// (C) 엔티티가 없어도 좌표로 새로 만든다
var e = _entities.TryGetValue(p.EntityId, out var ev)
? ev : (_entities[p.EntityId] = new EntityView { Id = p.EntityId });
e.X = p.X;
e.Y = p.Y;
break;
case MsgType.Death: // (D) 중요한 이벤트가 비신뢰 채널로
if (_entities.TryGetValue(p.EntityId, out var d))
d.Alive = false;
break;
}
}
} 결함 코드 · C++
// ============================================================================
// [코드리뷰 문제] C++ - 순서보장 채널 + 비순서 채널 혼용 시 상태 정합성
// ----------------------------------------------------------------------------
// 시나리오:
// - 게임은 두 종류의 채널을 함께 쓴다.
// * 신뢰·순서보장 채널(TCP류): 스폰/디스폰, 장비 변경, 사망 등 "중요 이벤트".
// * 비신뢰·비순서 채널(UDP류): 좌표 스냅샷(PosUpdate)처럼 고빈도·유실 허용.
// 두 채널은 서로 독립적이라, 채널 간 도착 순서는 보장되지 않는다(같은 채널
// 안에서도 비순서 채널은 재정렬/유실/중복 가능).
// - 각 패킷 와이어: [uint16 seq][uint8 type][payload], seq 는 채널별로 1씩 증가.
// - 클라이언트는 수신한 패킷을 엔티티 뷰(entities_)에 반영한다.
// - 서버는 좌표를 비신뢰 채널로, 스폰/디스폰/사망을 신뢰 채널로 보낸다.
//
// 요구사항:
// - 늦게/뒤늦게 도착한 옛 좌표 스냅샷이 최신 좌표를 덮어쓰면 안 된다.
// - 채널 간 인과관계(스폰 → 그 엔티티의 좌표)가 깨져도 유령/누락 엔티티가
// 생기면 안 된다.
// - 사망처럼 한 번 놓치면 상태가 영구히 어긋나는 이벤트는 유실되면 안 된다.
// - seq 는 uint16 이라 65536 마다 한 바퀴 돈다(wrap).
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 정합성이 깨지는지 설명하고,
// 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
#include <cstdint>
#include <unordered_map>
enum MsgType : uint8_t { SPAWN = 1, DESPAWN = 2, POS = 3, DEATH = 4 };
struct Payload
{
uint32_t entityId;
float x, y;
};
struct EntityView
{
uint32_t id;
float x, y;
bool alive;
};
class ClientWorld
{
public:
// 신뢰·순서보장 채널 수신 (TCP류) — 채널 내 순서는 보장됨
void OnReliable(uint16_t seq, uint8_t type, const Payload& p)
{
switch (type)
{
case SPAWN:
entities_[p.entityId] = EntityView{ p.entityId, p.x, p.y, true };
break;
case DESPAWN:
entities_.erase(p.entityId);
break;
}
}
// 비신뢰·비순서 채널 수신 (UDP류) — 재정렬/유실/중복 가능
void OnUnreliable(uint16_t seq, uint8_t type, const Payload& p)
{
switch (type)
{
case POS:
// (A) 전역 last seq 로만 최신 여부 판단
if (seq <= lastSeq_) // (B) wrap 미고려 비교
return;
lastSeq_ = seq;
// (C) 엔티티가 없어도 좌표로 새로 만든다
{
EntityView& e = entities_[p.entityId];
e.id = p.entityId;
e.x = p.x;
e.y = p.y;
}
break;
case DEATH: // (D) 중요한 이벤트가 비신뢰 채널로
entities_[p.entityId].alive = false;
break;
}
}
private:
std::unordered_map<uint32_t, EntityView> entities_;
uint16_t lastSeq_ = 0; // (E) 모든 엔티티/타입 공통 last seq
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.