2. 캐릭터 정보 패킷 직렬화 / 스키마 진화
난이도 중 해설 보기 →
결함을 모두 찾고 원인·수정안·더 나은 설계를 제시하라. 마커
(A)(B) 는 주목 위치 힌트다.
결함 코드 · C#
// ============================================================================
// [코드리뷰 문제] C# - 캐릭터 정보 패킷 직렬화/역직렬화 (스키마 진화)
// ----------------------------------------------------------------------------
// 시나리오:
// - 서버는 클라이언트에 캐릭터 정보(S_CharInfo)를 직렬화해 보낸다.
// - v1.0 출시 후, v1.1 에서 "길드ID" 필드를 추가하기로 했다.
// - 배포는 점진적이다: 서버를 먼저 v1.1 로 올리고, 클라이언트는
// 앱스토어 심사/사용자 업데이트 때문에 며칠~몇 주에 걸쳐 v1.0 과 v1.1 이 공존한다.
// - 즉, "v1.1 서버 ↔ v1.0 클라" 와 "v1.1 서버 ↔ v1.1 클라" 가 동시에 존재한다.
//
// 요구사항:
// - 구버전 클라이언트가 신버전 서버에 붙어도 캐릭터 정보를 정상 파싱해야 한다
// (적어도 크래시/연결 끊김은 없어야 한다).
// - 직렬화는 직접 작성한 바이너리 Writer/Reader 를 쓴다(아래).
//
// 아래는 v1.1 서버의 직렬화 코드와, 배포되어 현장에 남아있는 v1.0 클라의
// 역직렬화 코드다.
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 발생하는지(특히 버전 공존 시점),
// 수정안과 더 나은 스키마 진화 전략을 제시하라.
// ============================================================================
using System;
using System.Text;
// 공용 바이너리 라이터/리더 (양 진영이 공유)
public ref struct PacketWriter
{
private Span<byte> _buf;
public int Pos;
public PacketWriter(Span<byte> buf) { _buf = buf; Pos = 0; }
public void WriteInt(int v)
{
_buf[Pos++] = (byte)(v);
_buf[Pos++] = (byte)(v >> 8);
_buf[Pos++] = (byte)(v >> 16);
_buf[Pos++] = (byte)(v >> 24);
}
public void WriteString(string s)
{
byte[] bytes = Encoding.UTF8.GetBytes(s);
WriteInt(bytes.Length); // 길이
foreach (var b in bytes) _buf[Pos++] = b;
}
}
public ref struct PacketReader
{
private ReadOnlySpan<byte> _buf;
public int Pos;
public PacketReader(ReadOnlySpan<byte> buf) { _buf = buf; Pos = 0; }
public int ReadInt()
{
int v = _buf[Pos] | (_buf[Pos + 1] << 8) | (_buf[Pos + 2] << 16) | (_buf[Pos + 3] << 24);
Pos += 4;
return v;
}
public string ReadString()
{
int len = ReadInt();
var s = Encoding.UTF8.GetString(_buf.Slice(Pos, len));
Pos += len;
return s;
}
}
// ---------------------------------------------------------------------------
// v1.1 서버: 직렬화 (길드ID 필드 추가됨)
// ---------------------------------------------------------------------------
public class S_CharInfo_Server_v11
{
public int CharId;
public string Name;
public int Level;
public int GuildId; // (A) v1.1 에서 새로 추가한 필드
public void Serialize(PacketWriter w)
{
w.WriteInt(CharId);
w.WriteString(Name);
// (B)
w.WriteInt(GuildId);
w.WriteInt(Level);
}
}
// ---------------------------------------------------------------------------
// v1.0 클라이언트: 역직렬화 (길드ID 를 모른다 — 현장에 이미 배포됨)
// ---------------------------------------------------------------------------
public class S_CharInfo_Client_v10
{
public int CharId;
public string Name;
public int Level;
public void Deserialize(PacketReader r)
{
// (C) v1.0 클라는 필드 3개만 읽는다. 패킷에 버전 정보는 없다.
CharId = r.ReadInt();
Name = r.ReadString();
Level = r.ReadInt(); // (D)
}
} 결함 코드 · C++
// ============================================================================
// [코드리뷰 문제] C++ - 캐릭터 정보 패킷 직렬화/역직렬화 (스키마 진화)
// ----------------------------------------------------------------------------
// 시나리오:
// - 서버는 클라이언트에 캐릭터 정보(S_CharInfo)를 직렬화해 보낸다.
// - v1.0 출시 후, v1.1 에서 "길드ID" 필드를 추가하기로 했다.
// - 배포는 점진적이다: 서버를 먼저 v1.1 로 올리고, 클라이언트는
// 앱스토어 심사/사용자 업데이트 때문에 며칠~몇 주에 걸쳐 v1.0 과 v1.1 이 공존한다.
// - 즉, "v1.1 서버 ↔ v1.0 클라" 와 "v1.1 서버 ↔ v1.1 클라" 가 동시에 존재한다.
//
// 요구사항:
// - 구버전 클라이언트가 신버전 서버에 붙어도 캐릭터 정보를 정상 파싱해야 한다
// (적어도 크래시/연결 끊김은 없어야 한다).
// - 직렬화는 직접 작성한 바이너리 Writer/Reader 를 쓴다(아래).
//
// 아래는 v1.1 서버의 직렬화 코드와, 배포되어 현장에 남아있는 v1.0 클라의
// 역직렬화 코드다.
//
// 과제:
// 이 코드의 잠재적 문제를 모두 찾고, 왜/어떻게 발생하는지(특히 버전 공존 시점),
// 수정안과 더 나은 스키마 진화 전략을 제시하라.
// ============================================================================
#include <cstdint>
#include <cstring>
#include <string>
// 공용 바이너리 라이터/리더 (양 진영이 공유)
class PacketWriter {
public:
PacketWriter(uint8_t* buf) : _buf(buf), Pos(0) {}
int Pos;
void WriteInt(int32_t v)
{
// (W) 호스트 메모리 표현을 그대로 4바이트 쓴다
std::memcpy(_buf + Pos, &v, 4);
Pos += 4;
}
void WriteString(const std::string& s)
{
WriteInt((int32_t)s.size()); // 길이
std::memcpy(_buf + Pos, s.data(), s.size());
Pos += (int)s.size();
}
private:
uint8_t* _buf;
};
class PacketReader {
public:
PacketReader(const uint8_t* buf) : _buf(buf), Pos(0) {}
int Pos;
int32_t ReadInt()
{
// (R) 4바이트를 호스트 정수로 그대로 해석
int32_t v = *reinterpret_cast<const int32_t*>(_buf + Pos);
Pos += 4;
return v;
}
std::string ReadString()
{
int32_t len = ReadInt();
std::string s(reinterpret_cast<const char*>(_buf + Pos), len);
Pos += len;
return s;
}
private:
const uint8_t* _buf;
};
// ---------------------------------------------------------------------------
// v1.1 서버: 직렬화 (길드ID 필드 추가됨)
// ---------------------------------------------------------------------------
class S_CharInfo_Server_v11 {
public:
int32_t CharId;
std::string Name;
int32_t Level;
int32_t GuildId; // (A) v1.1 에서 새로 추가한 필드
void Serialize(PacketWriter& w)
{
w.WriteInt(CharId);
w.WriteString(Name);
// (B)
w.WriteInt(GuildId);
w.WriteInt(Level);
}
};
// ---------------------------------------------------------------------------
// v1.0 클라이언트: 역직렬화 (길드ID 를 모른다 — 현장에 이미 배포됨)
// ---------------------------------------------------------------------------
class S_CharInfo_Client_v10 {
public:
int32_t CharId;
std::string Name;
int32_t Level;
void Deserialize(PacketReader& r)
{
// (C) v1.0 클라는 필드 3개만 읽는다. 패킷에 버전 정보는 없다.
CharId = r.ReadInt();
Name = r.ReadString();
Level = r.ReadInt(); // (D)
}
}; 내 리뷰 · C#
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.
내 리뷰 · C++
내 답안 · 자동 저장
작성 후 위 해설 보기에서 모범 해설과 대조하세요.