패킷 처리 역시 Dawnstar에서 중요한 발전 과정 중 하나였습니다. 초기에는 직접 바이트를 읽고 쓰는 수동 직렬화 방식으로 시작했지만, 프로젝트가 커지면서 반복 작업이 많고 실수
가능성이 크다는 문제가 분명해졌습니다. 패킷 종류가 늘어날수록, 구조 변경 하나가 여러 곳의 수정으로 이어지고 디버깅도 어려워졌습니다.
그래서 중간 단계에서는 PacketGenerator를 이용해 반복되는 등록 코드와 패킷 매니저 생성을 자동화했고, 최종적으로는
Protocol.proto 기반의 Protobuf 패킷 구조로 정리했습니다. 현재 Dawnstar의 패킷 처리는 헤더는 직접 관리하고, 본문은 Protobuf로
직렬화하는 하이브리드 방식에 가깝습니다. 처음부터 이상적인 구조를 새로 만드는 것보다, 기존 파이프라인을 유지하면서 실제로 가장 문제가 되는 반복 작업과 오류 가능성을 줄이는 쪽이 더
현실적이었기 때문입니다.
ushort size = BitConverter.ToUInt16(buffer.Array, buffer.Offset);
ushort id = BitConverter.ToUInt16(buffer.Array, buffer.Offset + 2);
// Payload만 Protobuf로 역직렬화
pkt.MergeFrom(buffer.Array, buffer.Offset + 4, buffer.Count - 4);
헤더(Size, MsgId)는 직접 관리하고, 본문은 Protobuf로 역직렬화하여 유지보수와 확장성을 개선했습니다.
파이프라인 발전 과정 (3-Phase)
Phase 1: XML 스키마 정의 → 패킷 클래스 수동 작성 → 클라이언트/서버 양쪽에 Write/Read 로직 수동 구현. 패킷
구조 변경 시 양쪽 코드를 직접 수정해야 했고, 직렬화 순서가 어긋나면 치명적인 버그가 발생했습니다.
Phase 2: PacketFormat 템플릿 → PacketGenerator 실행 →
Server/Client PacketManager 및 핸들러 등록 코드 자동 생성. 반복 작업을 자동화했으나 직렬화/역직렬화 자체의 효율성과 버전 관리 한계는 남아있었습니다.
foreach (var msg in proto.Messages) {
if (msg.Name.StartsWith("S_"))
clientRegister += $"case MsgId.{msg.Name}: ...";
else if (msg.Name.StartsWith("C_"))
serverRegister += $"case MsgId.{msg.Name}: ...";
}
File.WriteAllText("ServerPacketManager.cs", serverManagerText);
PacketGenerator는 Protocol.proto를 기반으로 패킷 핸들러 등록 코드를 자동 생성합니다.
Phase 3: Protocol.proto 단일 소스 정의 → protoc 컴파일 →
PacketGenerator가 MsgId를 파싱하여 핸들러 매핑. 직렬화는 Protobuf에 위임하고, 기존 파이프라인은 핸들러 매핑 역할만 유지했습니다.
플로우 차트 보기
flowchart LR
subgraph Phase1["Phase 1: 초기"]
A1[XML 스키마 정의]
A2[패킷 클래스 수동 작성]
A3["Write/Read 수동 구현"]
A1 --> A2 --> A3
end
subgraph Phase2["Phase 2: 자동화"]
B1[PacketFormat 템플릿]
B2[PacketGenerator]
B3["Server/Client PacketManager 자동 생성"]
B1 --> B2 --> B3
end
subgraph Phase3["Phase 3: Protobuf"]
C1["Protocol.proto 정의"]
C2[protoc 컴파일]
C3["PacketGenerator MsgId 파싱"]
C1 --> C2 --> C3
end
Phase1 -->|개선| Phase2
Phase2 -->|전환| Phase3
최종 아키텍처
이 변화는 단순히 기술 스택이 좋아졌다는 의미보다, 프로젝트가 커질수록 유지보수가 가능한 형태로 옮겨갔다는 점에서 의미가 있었습니다.