나만의 작은 도서관
[TIL][C++] 250516 MMO 서버 개발 19일차: Protobuf keyword 재정리: Package, Optional, Oneof, Reserved 본문
Today I Learn
[TIL][C++] 250516 MMO 서버 개발 19일차: Protobuf keyword 재정리: Package, Optional, Oneof, Reserved
pledge24 2025. 5. 16. 23:21주의사항: 해당 글은 일기와 같은 기록용으로, 다듬지 않은 날것 그대로인 글입니다.
Protobuf keyword 재정리: Package, Optional, Oneof
package
- package는 이름 충돌을 예방하기 위한 키워드이다.
syntax = "proto3"
package Protocol;
message S_TEST { ... }
- 위와 같이 .proto 파일을 작성하면 protoc를 거쳐 나온 xxx.pb.cc 와 xxx.pb.h에서 package이름으로 namespace가 만들어지고 message들은 해당 namespace 안에 정의되어 있게 된다(C++기준)
- 프로젝트 내에선 아래와 같이 사용할 수 있다.
// package Protocol에 의해 namespace가 Protocol인 S_TEST 클래스 존재
Protocol::S_TEST pkt;
optional
- optional 키워드는 필드 앞에 붙일 수 있으며, optional 필드는 데이터를 선택적으로 추가할 수 있는, 즉, 데이터가 없을 수 있다.
- proto3에선 모든 필드는 선택적(optional)이기 때문에 optional을 붙이지 않아도 데이터가 없을 수 있다. 그럼 optional 키워드를 따로 붙일 필요가 없지 않으냐 할 수 있는데, optional 키워드를 붙였을 때 해당 필드에 추가적인 함수가 생긴다는 차이가 있다.
message S_TEST
{
int32 num1 = 1;
optional int32 num2 = 2;
}
- 위 메시지에서 num1과 num2는 int32 타입의 정수이다. num2의 경우 num1과 달리 optional이 붙었는데, 위에서도 말했지만 모든 필드는 선택적이기 때문에 num1과 num2 둘 다 데이터가 들어있을 거란 보장은 없다.
Protocol::S_TEST pkt;
pkt.has_num1(); // 안 됨
pkt.has_num2(); // 됨
- 이 둘의 차이점은 has_이름() 함수의 존재 차이이다. protobuf에서 string, bytes, bool, 숫자 타입, message 타입, enum 타입들에서 기본값을 가진다. 따라서, 데이터가 들어가지 않았다면 기본값으로 초기화된다.
- 문제는 기본값과 설정한 값이 같으면(ex. int32 타입의 필드를 0으로 설정) 값만으로는 설정했는지 알 수 없다. 이를 구분하기 위해 has_이름() 함수가 존재하며, optional 키워드를 붙인 경우 해당 함수가 추가된다.
Protocol::S_TEST pkt;
pkt.has_num2(); // false
pkt.set_num2(0);
pkt.has_num2(); // true
- 몇몇 타입들은 optional 키워드를 붙이지 않아도 자동 적용되어 has_이름() 함수를 가진다. 해당 타입들은 아래와 같다.
- 메시지 타입
- Oneof 내부의 모든 필드
- map 타입
- repeated 타입
- 반대로 optional 키워드를 붙여줘야 하는 타입은 아래와 같다.
- 스칼라 타입
- int32
- float
- bool
- string
- 기타 등등…
- 스칼라 타입
Oneof
- Oneof는 둘러싼 여러 필드 중에 하나만 설정되도록 하는 기능이며, 각 필드는 메모리를 공유한다.
message Result {
oneof result_type {
string text_result = 1;
int32 number_result = 2;
bool boolean_result = 3;
}
}
- 위 message에서 result_type에 존재하는 세 필드 중 text_result를 설정한 다음, number_result를 설정했다면 number_result 필드만 값을 가지고, text_result는 값이 존재하지 않는다. 왜냐하면 OneOf는 하나의 필드만 데이터를 가지므로, 마지막에 설정한 필드만이 데이터를 가지기 때문이다.
Reserved
- reserved 키워드는 필드 번호나 필드 이름을 예약하여 해당 메시지 내에서 사용하지 못하도록 막는 것이다.
// password_hash 삭제전
message UserProfile {
int32 id = 1;
string name = 2;
string email = 3;
string phone_number = 4;
string password_hash = 5;
}
// password_hash 삭제후 올바른 접근
message UserProfile {
int32 id = 1;
string name = 2;
string email = 3;
string phone_number = 4;
reserved 5; // password_hash 필드 번호 예약
reserved "password_hash"; // 필드 이름도 예약
string profile_picture_url = 6; // 새 필드는 다른 번호 사용
}
필드 번호를 reserved로 막는 이유
- 버전 호환성 문제를 방지하기 위함이다. reserved를 사용하지 않고 새 버전에서 같은 번호로 다른 의미의 필드를 만들면 데이터 해석이 꼬인다. ⇒ 메모리 오염도 추가 발생
- 어찌 보면 reinterpret_cast를 사용했을 때 발생하는 문제와 비슷하다 할 수 있다.
- 예시는 아래와 같다.
// 문제가 발생하는 예제
// 구버전 .proto파일
message Player {
int32 rank = 1;
}
// 신버전 .proto파일
message Player {
string name = 1; // rank가 쓰던 번호를 name이 재사용함 (❌ 위험)
}
클라가 구버전을 이용해 rank = 97을 보냄
서버가 신버전을 이용해 97을 name으로 해석 -> "a"
=> 의도치 않은 작동 발생(메모리 오염)
reserved를 이용한 올바른 예제
// 구버전 .proto파일
message Player {
int32 rank = 1;
}
// 신버전 .proto파일
message Player {
reserved 1;
reserved "rank"
string name = 2; // rank가 쓰던 번호를 name이 재사용함 (❌ 위험)
}
클라가 구버전을 이용해 rank = 97을 보냄
서버가 신버전을 이용했을때 예약된 필드 번호 1로 데이터가 온 것을 감지
=> 오류 발생(버그 예방)