목표
이번 글에서는 최근 많은 개발자들 사이에서 화두가 되고 있는 Protobuf에 대해 알아보려고 합니다. Protobuf는 어떤 것이며 기존의 데이터 포맷(예: JSON, XML)과 어떤 차이점이 있는지에 대해 알아보겠습니다.
Protobuf란 무엇인가?
Protobuf, 또는 '프로토콜 버퍼'는 Google에서 개발한 이진 데이터 포맷입니다. 구조화된 데이터를 직렬화하고 역직렬화하는 데 사용되며, 효율적이고 빠르게 데이터를 전송하고 저장할 수 있습니다.
Protobuf의 특징
- 이진 포맷: Protobuf는 이진 데이터 포맷을 사용하여, 사람이 읽을 수 있는 포맷보다 훨씬 더 작고 빠르게 데이터를 전송합니다.
- 언어 중립: 여러 프로그래밍 언어에 대한 지원이 포함되어 있어 다양한 언어로 개발된 시스템 간의 통신에 유용합니다.
- 확장 가능: 기존의 정의된 데이터 구조에 새로운 필드를 추가하더라도 이전 버전과의 호환성을 유지할 수 있습니다.
Protobuf vs JSON/XML
- 크기: Protobuf는 JSON이나 XML보다 데이터 크기가 훨씬 작습니다.
- 성능: 직렬화 및 역직렬화 속도가 더 빠릅니다.
- 타입 안정성: 스키마를 사용하여 데이터 구조와 타입을 명확하게 정의할 수 있습니다.
이렇게, Protobuf는 효율적인 데이터 전송 및 저장을 위한 강력한 도구로, 많은 대규모 시스템과 서비스에서 선호되는 방식입니다. 다음 포스트에서는 Protobuf의 기본적인 사용 방법과 함께 실전 예제를 통해 어떻게 적용할 수 있는지에 대해 알아보도록 하겠습니다.
Protobuf 문법
syntax = "proto2"; // proto 자체의 문법 버전
package sj; // C++에서 namespace sj; 가 됩니다.
message Person { // C++에서 class Person 가 됩니다.
required string name = 1; // 반드시 지정해야되는 필드. 추후에 스키마에서 삭제될 수 없습니다.
optional int32 id = 2; // 숫자는 내부적으로 이 필드를 구분하기 위한 번호입니다.
enum PhoneType { // C++에서 class Person 안의 enum이 됩니다.
MOBILE = 0; // enum 상수 값
HOME = 1;
}
message PhoneNumber { // class Person 안에 nested class 로 class PhoneNumber
optional string number = 1;
optional PhoneType type = 2 [default = HOME]; // 기본 값 지정
}
// 배열 형태 필드 선언. 0개면 없는 것이나 마찬가지이므로 암묵적으로 optional
repeated PhoneNumber phones = 4;
}
- package는 프로그래밍 언어별로 모듈을 구분할 수 있는 정보가 됩니다.
- C++에서는 namespace 가 됩니다.
- Java에서는 package 이름이 됩니다.
- Python에서는 그냥 무시됩니다.
- message는 class로 만들어집니다.
- message 안에 있는 message는 class 안의 nested class가 됩니다.
- field 앞에는 관례적으로 optional을 붙입니다.
- 설정되지 않아도 되는 field라는 뜻으로, 나중에 field가 삭제되더라도 영향이 없습니다.
- required는 절대 안 바꿀 field에만 기재합니다.
- 이 값이 지정되지 않으면 serialize/deserialize 메서드들은 실패합니다.
- field가 배열 형태라면 optional 대신 repeated를 붙입니다.
- field의 타입은 다음이 가능합니다
- bool
- 정수형으로 int32, int64, uint32, uint64
- 소수형으로 float, double
- 문자형으로 string
- field 이름 뒤에 "= 숫자"는 binary로 serialize/deserialize 될 때 내부적으로 field 번호로 쓰입니다.
효율성을 위해 자주 쓰는 것을 적은 숫자로 배정합니다. - "[default = XXX]" 형태로 field에 기본 값을 지정할 수 있습니다.
- enum 타입 지정이 가능합니다.
- 각 프로그래밍 언어별로 enum 상수들이 상수값으로 변환됩니다.
- C++에서는 마찬가지로 enum 상수가 됩니다.
- Python에서는 그냥 상수가 됩니다.
- 각 프로그래밍 언어별로 enum 상수들이 상수값으로 변환됩니다.
proto 파일 컴파일
proto 파일은 클래스 생성을 위한 schema에 해당합니다.
protoc라는 프로그램을 통해 실제 프로그래밍 언어별 파일을 생성해야 됩니다.
- C++ 클래스 생성
$ protoc -I. --cpp_out=. person.proto
- Python 클래스 생성
$ protoc -I. --python_out=. person.proto
참고
- -I : proto 파일이 다른 proto 파일을 포함할 때, 파일을 찾는 상대 경로로 쓰입니다.
- --cpp_out : c++ 파일을 생성하는데, 그 디렉터리를 지정
- --python_out : python 파일을 생성하며, 그 디렉터리를 지정
C++에서 Serialize 해보기
//test.cpp
#include <fstream>
#include <string>
#include <iostream>
#include "person.pb.h"
using namespace std;
using namespace sj;
int main() {
Person *p = new Person;
p->set_name("DK moon");
p->set_id(12345678);
Person::PhoneNumber* phone = p->add_phones();
phone->set_number("010-111-1234");
phone->set_type(Person::MOBILE);
phone = p->add_phones();
phone->set_number("02-100-1000");
phone->set_type(Person::HOME);
const string s = p->SerializeAsString();
cout << "Length:" << s.length() << endl;
cout << s << endl;
터미널을 열고 다음처럼 실행파일을 생성한 후 실행해 봅니다.
주의: protobuf라는 외부 라이브러리를 써야 하므로 VS Cods의 실행 기능으로는 안됩니다.
$ g++ -o test test.cpp person.pb.cc -lprotobuf
$ ./test
Serialize 된 결과가 text가 아니라 binary임을 확인할 수 있습니다.
참고: text 문자열이 있다고 전체가 text가 아닙니다. ID에 해당하는 정수값이 binary로 표시되었으므로 우리가 읽을 수 없음을 확인할 수 있습니다.
우리가 정하는 필드의 크기보다 약간 더 큰 데이터를 저장하고 있는 것도 확인할 수 있습니다.
- 이름: 7바이트
- 아이디: 4바이트 정수
- 모바일 전화: 11바이트
- 집 전화: 10바이트
- Total: 31바이트
Encoding 된 바이트: 49바이트
C++에서 Deserialize 해보기
위 코드를 살짝 수정해 보겠습니다.
//test.cpp
#include <fstream>
#include <string>
#include <iostream>
#include "person.pb.h"
using namespace std;
using namespace sj;
int main() {
Person *p = new Person;
p->set_name("DK moon");
p->set_id(12345678);
Person::PhoneNumber* phone = p->add_phones();
phone->set_number("010-111-1234");
phone->set_type(Person::MOBILE);
phone = p->add_phones();
phone->set_number("02-100-1000");
phone->set_type(Person::HOME);
const string s = p->SerializeAsString();
cout << "Length:" << s.length() << endl;
cout << s << endl;
// 추가된 코드
Person *p2 = new Person;
p2->ParseFromString(s);
cout << "Name:" << p2->name() << endl;
cout << "ID:" << p2->id() << endl;
for(int i = 0; i < p2->phones_size(); ++i) {
cout << "Type:" << p2->phones(i).type() << endl;
cout << "Phone:" << p2->phones(i).number() << endl;
}
}
위 코드를 아래 명령어로 빌드 후 실행해 보겠습니다.
$ g++ -o test test.cpp person.pb.cc -lprotobuf
$ ./test
제대로 deserialize 됨을 확인할 수 있습니다.
앞의 실습을 통해 serialize 된 문자열 s를 (UDP, TCP) 네트워크로 전송하면
받는 쪽에서 deserialize 할 수 있음을 확인했습니다.
참고: Protobuf는 endian을 고려해서 serialize/deserialize 하므로 따로 htons(), htonl()은 할 필요가 없습니다.
다른 언어와의 호환성
Protobuf의 serialize 된 결과물은 여러 프로그래밍 언어와 호환됩니다.
//test.cpp
#include <fstream>
#include <string>
#include <iostream>
#include "person.pb.h"
using namespace std;
using namespace sj;
int main() {
Person *p = new Person;
p->set_name("DK moon");
p->set_id(12345678);
Person::PhoneNumber* phone = p->add_phones();
phone->set_number("010-111-1234");
phone->set_type(Person::MOBILE);
phone = p->add_phones();
phone->set_number("02-100-1000");
phone->set_type(Person::HOME);
const string s = p->SerializeAsString();
cout << "Length:" << s.length() << endl;
cout << s << endl;
Person *p2 = new Person;
p2->ParseFromString(s);
cout << "Name:" << p2->name() << endl;
cout << "ID:" << p2->id() << endl;
for(int i = 0; i < p2->phones_size(); ++i) {
cout << "Type:" << p2->phones(i).type() << endl;
cout << "Phone:" << p2->phones(i).number() << endl;
}
// 추가된 코드
ofstream f("mybinary", ios_base::out | ios_base::binary);
f << s;
}
ofstream f("mybinary", ios_base::out | ios_base::binary);
f << s;
ofstream을 사용하여 직렬화된 데이터를 이진 형태로 파일에 저장합니다. 저장될 파일의 이름은 "mybinary"입니다.
이 코드를 실행하면 mybinary파일이 생성됩니다.
import person_pb2
import sys
def main(argv):
with open('mybinary', mode='rb') as f:
s = f.read()
p = person_pb2.Person()
p.ParseFromString(s)
print('Name', p.name)
print('Id', p.id)
print('Phone1', p.phones[0].type, p.phones[0].number)
print(p)
if __name__ == '__main__':
main(sys.argv)
이 코드는 Python으로 Protobuf 라이브러리를 이용하여, 'Person'이라는 데이터 구조를 역직렬화하는 코드입니다. C++의 Person 객체를 Python에서도 호환하는 것을 확인할 수 있습니다.
앞의 실습을 통해 프로그래밍 언어에 관계없이 protobuf로 상호 통신할 수 있음을 확인했습니다. 여기서는 단순히 파일로 통신했으나, 네트워크로 전송할 때도 동일합니다.
주의: Protobuf로 serialize 한 결과물은 binary입니다. 따라서 파일 작업을 할 때도 "binary" 모드로 작업해야 됩니다.
- C++에서는 ios_base::binary 옵션
- Python에서는 mode='b' 옵션
JSON Serialize
JSON이란?
JSON은 JavaScript Object Notation의 약자로, 데이터를 저장하거나 전송할 때 주로 사용되는 경량의 데이터 형식입니다. 원래 JavaScript에서 객체를 표현하기 위한 표기법인데, 현재는 다양한 프로그래밍 언어에서 데이터 교환 형식으로 널리 사용되고 있습니다.
- JavaScript 객체 표기법에 기반
- Tag 방식이 아니라 Key-Value 쌍 형태
- 예: {"department": "컴퓨터공학과", "name": "박세진"}
- 장점
- XML에 비해 적은 사이즈
- XML에 비해 더 빠른 parsing
- XML에 비해 사람이 읽기에 더 높은 가독성
- 프로그래밍 언어의 dictionary 혹은 map 등과 유사한 표기 사용
#import json
#import sys
def main(argv):
obj1 = {
'name': 'DK Moon',
'id': 12345678,
}
s = json.dumps(obj1)
print(s)
print('Type', type(s).__name__)
if __name__ == '__main__':
main(sys.argv)
출력된 결과를 보면 JSON Object는 {...}로 감싸집니다. 또한 string 타입임을 알 수 있습니다.
이는 Protobuf가 binary 인 것과 확연한 차이점입니다.
또한 대응되는 value의 타입에 관계없이 key는 언제나 문자열 형태로 씁니다.
그리고 출력되는 JSON 결과 문자열에서 name이라는 key와 id라는 key에 대응되는 value가
각각 string, number라는 다른 타입을 가지고 있습니다.
이처럼 JSON object의 key들은 이질적인 value type을 가질 수 있습니다. 심지어 또 다른 JSON object를 value로 가질 수도 있습니다.
'CS' 카테고리의 다른 글
[Network] TCP Socket Programming - 데이터 수신하기 (1) | 2023.10.22 |
---|---|
[Network] TCP Socket Programming - 데이터 전송하기 (1) | 2023.10.22 |
[Network] Socket Programming 메모리 관리 (1) | 2023.10.21 |
[Network] UDP Socket 만들어보기 - 데이터 받기 (1) | 2023.10.20 |
[Network] UDP Socket 만들어보기 - 데이터 보내기 (3) | 2023.10.15 |