프로토콜 버퍼(Protobuf)
2025년 7월 16일
프로토콜 버퍼(Protocol buffer, 줄여서 protobuf)는 구조적인 데이터를 직렬화하는 매커니즘1입니다. 여기서 직렬화란 구조적인 데이터를 저장 또는 공유를 목적으로 이진화하는 기법으로, 원래의 데이터에서 이진화된 데이터로 변환하고, 이진화된 데이터에서 원래의 데이터로 변환할 때 완전한 복구가 가능해야 합니다.
예를 들어, 멀리 떨어진 두 컴퓨터 사이에 데이터를 전달할 때, 키-값 구조나 배열과 같은 구조적인 정보가 포함된 데이터를 이진값만 보낼 수 있는 케이블을 통해 전달하려면 보내는 측에서 그 데이터를 이진수로 나타내어 전달하고, 받는 측에서는 받은 데이터를 다시 원래의 데이터로 복원하는 과정이 필요합니다. 이 때 데이터의 변형이나 손실이 발생한다면 통신이 제대로 이루어지지 않기 때문에, 따라서 데이터의 공유/저장을 목적으로 사용되는 데이터 직렬화는 반드시 원래의 데이터와 직렬화된 데이터가 다른 한쪽으로부터 완전하게 복구될 수 있어야 하는 일대일 대응을 보장해야 합니다.
보통은 키-값 맵이나 배열 형태의 구조화된 데이터를 저장할 때 XML이나 json과 같은 사람이 읽을 수 있는 포맷이 존재하고, 쉽게 사용할 수 있습니다. 프로토콜 버퍼는 이들에 비해 더 작고, 빠르게 직렬화되도록 구현한 기법이라고 할 수 있습니다. 예를 들어, API 서버에서 쓰이는 RESTful 프레임워크는 json을 통해 데이터의 전달이 이루어지는 반면, gRPC 프레임워크에서는 protobuf 직렬화를 도입하여 더 빠른 데이터 전달이 가능하도록 만들어지기도 하였습니다.
인코딩 방식
protobuf가 어떻게 데이터를 직렬화하는 지에 대해 다룹니다2.
정수값 표현
모든 정수를 표현할 수 있는 기법을 사용합니다. 통상적인 32비트 정수, 64비트 정수와 같은 고정폭 정수들과 다르게, 정수가 차지하는 공간의 크기가 정해지지 않습니다. 대신에, 정수의 크기가 커질수록 차지하는 공간도 커지게 됩니다.
구체적으로, 각 바이트에는 최상위 비트(MSB)가 continuation 비트의 역할을 해서, 해당 비트가 켜져 있으면 다음 바이트도 연속해서 읽어 정수값에 반영이 되고, 꺼져 있으면 현재 바이트까지가 정수를 나타내는 이진값으로 해석됩니다. 한편, 최상위 비트를 제외한 나머지 영역은 모두 이진수의 데이터를 나타냅니다.
0000 0001
^ msb
이 경우에는 000
0001
까지가 실제 정수값을 나타내는 데이터로, 맨 앞의 0은 해당 정수 데이터가 현재 바이트까지이고, 이후에 오는 바이트는 다른 데이터를 나타낸다는 것을 의미합니다. 따라서 이 값은 이진수 0000001에 대응되고, 따라서 1입니다.
1001 0110 0000 0001
^msb1 ^msb2
이 경우, 001 0110
과 000 0001
가 실제 정수값을 나타냅니다. protobuf는 최상위 바이트가 가장 나중에 오는 little-endian 방식을 사용하므로, 사람이 읽을 때는 000 0001 001 0110
으로 big-endian 형태로 순서를 변경해야 하고, 이진수 10010110은 150에 대응됩니다.
지금까지 정수값의 디코딩에 대해 설명했는데, 인코딩도 간단하게 구현이 가능하다는 점도 생각해보면 알 수 있습니다.
메시지 구조
메시지가 인코딩되면, 각 키-값 쌍은 필드 넘버, 와이어 타입, 그리고 페이로드로 변환이 됩니다. 와이터 타입에 뒤에 오는 페이로드가 얼마나 큰지를 알려주는데, 이것은 필요에 따라 해석이 안되는 필드를 건너뛸 수 있게 해주는 힌트가 되기도 합니다. 와이어 타입은 다음과 같은 종류가 있습니다.
message Test1 {
int32 a = 150;
}
예를 들어, 이런 메시지에서는 1번째 빌드에 i32 타입의 정수 1이 담겨있는데, 변환하면 다음과 같습니다:
00001000 10010110 00000001
앞서 정수 데이터 150은 뒤의 두 바이트를 의미한다는 것을 확인했으니, 00001000
의 의미만 파악하면 됩니다.
0 0001 000
msb | field number | wire type
이런 식으로, 첫 세자리는 와이어 타입을, 나머지는 필드 넘버를 나타내게 됩니다. 즉, 첫 번째 필드의 타입이 000이고, 000은 임의의 길이의 정수를 나타내는 타입인 VARINT이므로, 첫 번째 필드에 VARINT가 저장된다는 점을 알 수 있습니다.
하지만 원래 메시지에 있던, ‘Test1’, ‘int32’, ‘a’와 같은 구체적인 정보는 직렬화된 데이터에
포함되지 않습니다. 이들은 protobuf를 정의할 때 사용하는 .proto
스키마 파일이 같이 있을 때 복구가 가능합니다. 데이터를 정말로 필요한 만큼만 최소한으로 전달하고, 잘 변하지 않는 스키마는 서버와 클라이언트가 미리 공유할 수 있기 때문입니다.