19. 채널/인스턴스 이동 중 상태 이관 누락
난이도 중 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 채널/인스턴스 이동 중 상태 이관 누락
// ----------------------------------------------------------------------------
// 시나리오 (세션/네트워크 · 서버-서버):
// - 플레이어가 채널/인스턴스(예: 마을 채널 1 -> 채널 2, 또는 던전 인스턴스)로 이동한다.
// - 이동은 "출발 서버(source)" 가 플레이어 상태를 직렬화해 "대상 서버(target)" 로 넘기는
// 핸드오프로 처리된다. 대상 서버가 상태를 받아 플레이어를 활성화하면 이동 완료.
// - 플레이어의 휘발 상태에는 버프(남은 지속시간), 스킬 쿨다운, 진행 중 시전(cast),
// 수령 대기 전리품 등이 있다. 이동 후에도 이 상태는 그대로 유지돼야 한다.
// - 핸드오프 도중에도 출발 서버에는 이 플레이어를 향한 패킷(데미지/버프/채팅)이 계속 들어온다.
//
// 요구사항:
// - 휘발 상태(버프 남은시간·쿨다운·진행 중 시전 등)가 이동 후 유실되면 안 된다.
// - 어느 순간에도 같은 플레이어가 두 서버에서 동시에 활성(중복 처리)되면 안 되고,
// 반대로 양쪽 모두 비활성(패킷 블랙홀)인 창도 최소화해야 한다.
// - 핸드오프 중 출발 서버에 들어온 갱신이 사라지면 안 된다.
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 상태가 유실되거나 중복/블랙홀 활성이
// 생기는지 설명하고, 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
using System;
using System.Collections.Generic;
public class Buff
{
public int BuffId;
public long ExpireAtMs; // 만료 절대시각
}
public class PlayerState
{
public long PlayerId;
public int Hp;
public List<Buff> Buffs = new();
public Dictionary<int, long> CooldownUntilMs = new(); // skillId -> 쿨다운 종료시각
// 진행 중 시전(있으면 비어있지 않음)
public int CastingSkillId; // 0 = 없음
}
// 직렬화돼 대상 서버로 전송되는 이관 페이로드
public class MigrationPayload
{
public long PlayerId;
public int Hp;
public List<Buff> Buffs;
}
public class MigrationService
{
private readonly Dictionary<long, PlayerState> _active = new(); // 이 서버에서 활성 플레이어
private readonly Func<string, MigrationPayload, bool> _sendToTarget; // (targetServer, payload)
public MigrationService(Func<string, MigrationPayload, bool> sendToTarget)
{
_sendToTarget = sendToTarget;
}
// 출발 서버: 플레이어를 대상 서버로 이관 시작
public bool BeginMigrate(long playerId, string targetServer)
{
if (!_active.TryGetValue(playerId, out var st)) return false;
// (A) 휘발 상태 중 일부만 직렬화 (Hp, Buffs 만)
var payload = new MigrationPayload
{
PlayerId = st.PlayerId,
Hp = st.Hp,
Buffs = st.Buffs,
};
// (B) 대상으로 보내기 전에 출발 서버에서 먼저 세션 제거
_active.Remove(playerId);
// (C) 전송 (성공/실패 무시)
_sendToTarget(targetServer, payload);
return true;
}
// 출발 서버: 핸드오프 도중에도 들어오는 플레이어 갱신
public void ApplyUpdate(long playerId, Action<PlayerState> mutate)
{
if (!_active.TryGetValue(playerId, out var st)) return; // 없으면 그냥 무시
mutate(st);
}
// 대상 서버: 이관 페이로드 수신 -> 활성화
public void OnReceiveMigration(MigrationPayload payload)
{
var st = new PlayerState
{
PlayerId = payload.PlayerId,
Hp = payload.Hp,
Buffs = payload.Buffs ?? new List<Buff>(),
// CooldownUntilMs, CastingSkillId 는 페이로드에 없어 초기화됨
};
_active[payload.PlayerId] = st;
}
} 결함 코드 · C++
// ============================================================================
// [코드리뷰 문제] C++ - 채널/인스턴스 이동 중 상태 이관 누락
// ----------------------------------------------------------------------------
// 시나리오 (세션/네트워크 · 서버-서버):
// - 플레이어가 채널/인스턴스로 이동한다. "출발 서버(source)" 가 플레이어 상태를
// 직렬화해 "대상 서버(target)" 로 넘기는 핸드오프로 처리된다.
// - 휘발 상태: 버프(남은 지속시간), 스킬 쿨다운, 진행 중 시전(cast). 이동 후에도 유지돼야 한다.
// - 핸드오프 도중에도 출발 서버에는 이 플레이어를 향한 패킷이 계속 들어온다(applyUpdate).
//
// 요구사항:
// - 휘발 상태가 이동 후 유실되면 안 된다.
// - 어느 순간에도 같은 플레이어가 두 서버에서 동시 활성(중복 처리)되면 안 되고,
// 양쪽 모두 비활성(블랙홀)인 창도 최소화한다.
// - 핸드오프 중 출발 서버에 들어온 갱신이 사라지면 안 된다.
// - 이관 도중에 들어온 처리(applyUpdate)가 이미 해제된 세션을 건드려선 안 된다(UAF 금지).
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 상태 유실·블랙홀·중복 활성·UAF 가
// 생기는지 설명하고, 수정안과 더 나은 설계를 제시하라.
// (먼저 직접 리뷰를 적은 뒤 answer.md 와 대조할 것)
// ============================================================================
#include <cstdint>
#include <vector>
#include <unordered_map>
#include <memory>
#include <functional>
struct Buff {
int buffId;
int64_t expireAtMs;
};
struct PlayerState {
int64_t playerId;
int hp;
std::vector<Buff> buffs;
std::unordered_map<int, int64_t> cooldownUntilMs; // skillId -> 쿨다운 종료
int castingSkillId = 0; // 0 = 없음
};
// 직렬화돼 대상 서버로 전송되는 이관 페이로드
struct MigrationPayload {
int64_t playerId;
int hp;
std::vector<Buff> buffs;
};
class MigrationService {
public:
explicit MigrationService(std::function<bool(const char*, const MigrationPayload&)> sendToTarget)
: sendToTarget_(std::move(sendToTarget)) {}
// 출발 서버: 이관 시작
bool beginMigrate(int64_t playerId, const char* targetServer) {
auto it = active_.find(playerId);
if (it == active_.end()) return false;
PlayerState* st = it->second.get();
// (A) 휘발 상태 일부만 직렬화 (hp, buffs 만)
MigrationPayload payload{ st->playerId, st->hp, st->buffs };
// (B) 대상 전송 전에 출발 서버에서 먼저 세션 해제 (unique_ptr 파괴)
active_.erase(it);
// (C) 전송 (성공/실패 무시)
sendToTarget_(targetServer, payload);
return true;
}
// 출발 서버: 핸드오프 도중에도 들어오는 갱신
void applyUpdate(int64_t playerId, const std::function<void(PlayerState&)>& mutate) {
auto it = active_.find(playerId);
if (it == active_.end()) return; // 없으면 무시
mutate(*it->second);
}
// 대상 서버: 페이로드 수신 -> 활성화
void onReceiveMigration(const MigrationPayload& payload) {
auto st = std::make_unique<PlayerState>();
st->playerId = payload.playerId;
st->hp = payload.hp;
st->buffs = payload.buffs;
// cooldownUntilMs, castingSkillId 는 페이로드에 없어 기본값(빈/0)
active_[payload.playerId] = std::move(st);
}
private:
std::unordered_map<int64_t, std::unique_ptr<PlayerState>> active_;
std::function<bool(const char*, const MigrationPayload&)> sendToTarget_;
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.