나만의 작은 도서관
[TIL][C++] 250902 MMO 서버 개발 92일차: 프로토버프 Oneof에 대해 자세히 알아보기, 프로토버프의 직렬화 원리(일부) 본문
Today I Learn
[TIL][C++] 250902 MMO 서버 개발 92일차: 프로토버프 Oneof에 대해 자세히 알아보기, 프로토버프의 직렬화 원리(일부)
pledge24 2025. 9. 2. 21:13주의사항: 해당 글은 일기와 같은 기록용으로, 다듬지 않은 날것 그대로인 글입니다.
프로토버프 Oneof에 대해 자세히 알아보기
oneof란?
- 여러 필드 중 최대 하나의 필드만 설정될 수 있도록 하는 구조. 아래와 같은 메시지가 존재했을 때 name, id, flag 중 단 하나의 필드만 값이 존재하게 된다.
syntax = "proto3";
message TestMessage {
oneof test_oneof {
string name = 1;
int32 id = 2;
bool flag = 3;
}
}
사용 예시
Example msg;
// 값 설정(예시를 보면 알 수 있득, 각 필드는 oneof 이름으로 접근하지는 않는다.)
msg.set_name("홍길동");
// 이제 msg.id()는 사용할 수 없고, name만 유효함
// 다른 값 설정
msg.set_id(123);
// 이제 name은 자동으로 clear됨
주의할 점 1. 어느 필드도 값이 존재하지 않을 수 있다.
- 아예 데이터를 넣지 않았다면, oneof로 묶은 필드라도 데이터가 존재하지 않을 수 있다.
주의할 점 2. 기본값이 존재하지 않는다.
- 프로토버프는 기본적으로 기본값이 내장되어 있는데, oneof의 경우 기본값이 존재하지 않는다. 즉, oneof의 string, int32, bool과 같은 타입의 필드는 “”, 0, false로 초기화되지 않는다.
C++에서는 oneof 필드 중 어느 필드가 설정되었는지 알 수 있을까?
- 프로토버프가 지원하는 프로그래밍 언어는 다양하기 때문에 각 언어마다 각 상황을 처리하는 방식이 다르다. 그래서 같은 oneof를 처리하는 경우에도 서로 다른 방식으로 처리한다.
- C++의 경우 oneof 필드 중 어느 필드가 설정되어 있는지 알려면 enum getter를 사용한다.
// proto
message Example {
oneof test_oneof {
string name = 1;
int32 id = 2;
}
}
// c++
Example msg;
msg.set_name("홍길동");
switch (msg.test_oneof_case()) {
case Example::kName:
std::cout << "현재 name이 설정됨: " << msg.name() << std::endl;
break;
case Example::kId:
std::cout << "현재 id가 설정됨: " << msg.id() << std::endl;
break;
case Example::TEST_ONEOF_NOT_SET:
std::cout << "아무 것도 설정 안 됨" << std::endl;
break;
}
- 위 예시처럼 C++은 xxx_case()와 같은 함수로 프로토버프 내부적으로 설정된 enum값을 반환받아 어느 필드가 설정되어 있는지 알 수 있다. 비교할 enum은 각 필드 앞에 ‘k’가 붙은 이름을 가진다.(Example::kName처럼)
프로토버프의 직렬화 원리(일부)
- 직렬화 형식은 각 필드가 key, value 쌍으로 저장되는 형식이며, 프로토버프는 세팅되지 않은 필드는 메시지에 포함되지 않는다.
- 따라서 직렬화 시 전송하는 바이트배열에 용량을 차지하지 않는다.
#include <iostream>
#include <string>
#include "example.pb.h" // protoc로 생성된 헤더
int main() {
Example msg;
// 아무 것도 세팅하지 않음
std::string buffer;
msg.SerializeToString(&buffer);
std::cout << "Empty message size: " << buffer.size() << " bytes\\n";
// id 필드에 기본값(0) 세팅
msg.set_id(0);
buffer.clear();
msg.SerializeToString(&buffer);
std::cout << "With id=0 size: " << buffer.size() << " bytes\\n";
// id 필드에 123 세팅
msg.set_id(123);
buffer.clear();
msg.SerializeToString(&buffer);
std::cout << "With id=123 size: " << buffer.size() << " bytes\\n";
// name 필드에 빈 문자열("") 세팅
msg.clear_id();
msg.set_name("");
buffer.clear();
msg.SerializeToString(&buffer);
std::cout << "With name=\\"\\" size: " << buffer.size() << " bytes\\n";
// name 필드에 "hi" 세팅
msg.set_name("hi");
buffer.clear();
msg.SerializeToString(&buffer);
std::cout << "With name=\\"hi\\" size: " << buffer.size() << " bytes\\n";
return 0;
}
// 출력 결과
Empty message size: 0 bytes
With id=0 size: 0 bytes
With id=123 size: 2 bytes
With name="" size: 0 bytes
With name="hi" size: 4 bytes
