gRPC 기초 : Protocol BuffersgRPC Basics: Protocol Buffers
gRPC 기본 요소 Protocol BuffersProtocol Buffers, the building block of gRPC
gRPC에 사용되는 Protocol Buffers를 알아보겠습니다.
Protocol Buffers
Protocol Buffers는 구글에서 만든 이진 직렬화 포맷입니다. 줄여서 protobuf라고 부릅니다. JSON, YAML, XML과 같은 데이터를 표현하는 포맷입니다. 익숙한 포맷들과 가장 큰 차이는 데이터를 이진 데이터로 표현한다는 점입니다. protobuf의 장단점은 아래와 같습니다.
장점
- 직렬화와 역직렬화가 매우 빠름
- 데이터 크기가 작아 네트워크 전송이 효율적
- 다양한 프로그래밍 언어(Java, Python, C++, Go 등)에서 사용 가능
- 컴파일 시점에 타입 체크를 수행하여 런타임 오류
단점
- 이진 형식이라 사람이 직접 읽기 어렵고 디버깅이 어려움
- .proto 파일 작성과 protobuf 사용법을 학습
- 웹 브라우저에서 직접 사용하기 어렵움
- 간단한 데이터 교환의 경우, JSON이나 XML에 비해 과도하게 복잡
protobuf 설치
gRPC에 나와있는 설치 방법 페이지를 참고하여 설치를 진행합니다. 아래의 세 가지 방법을 추천합니다. 여기서는 python 모듈을 설치하도록 하겠습니다.
- 각 언어별 모듈 설치
- brew 이용
- 컴파일된 바이너리 다운로드
apt를 통한 설치
반드시 최신의 버젼인지 확인 합니다. apt를 통한 설치는 추천하지 않습니다.
# 추천하지 않음
$ apt install protobuf-compiler
python 모듈 설치
- Python
3.7이상 - pip version 9.0.1 이상
$ pip install grpcio-tools
$ python -m grpc_tools.protoc --version
.proto 작성
우선 가장 먼저 .proto 라는 스키마 파일을 작성해야 합니다. 해당 파일을 컴파일 하여 각 언어별로 protobuf를 사용할 수 있는 파일을 만들 수 있습니다. 일종의 인터페이스 코드를 생성 한다고 생각하면 됩니다.
기본적인 JSON 형태의 데이터를 .proto로 비교하면서 작성해보겠습니다.
JSON
{
"person": {
"name": "tiaz0128"
}
}
.proto 작성
protos 폴더 아래 person.proto라는 파일 작성하겠습니다.
syntax = "proto3" 3버전을 사용하겠다는 의미이며 주로 3버젼을 사용합니다. syntax 구문을 작성하지 않으면 2버젼을 기본으로 사용하기 때문에 반드시 작성해 줍니다.
syntax = "proto3";
message Person {
string name = 1;
}
message Data {
Person person = 1;
}
message 작성법
message는 protobuf의 핵심 구성 요소로, 데이터 구조를 정의하는 데 사용됩니다. 기본적인 작성법에 대해서 알아보겠습니다.
코드
syntax = "proto3";
message Person {
string name = 1;
int32 age = 2;
enum Gender {
UNKNOWN = 0;
MALE = 1;
FEMALE = 2;
}
Gender gender = 3;
repeated string hobbies = 4;
message Address {
string street = 1;
string city = 2;
}
Address address = 5;
}
기본 구조
string name = 1;
- 중괄호
{}안에 필드들을 정의 - message 키워드로 시작하고, 메시지 이름을 지정
- 각 필드는
타입 이름 = 번호;형식으로 정의 - 각 필드에는 고유한 번호를 할당 (1-536,870,911 범위).
- 1-15 범위의 번호는 인코딩 시 1바이트만 사용하므로, 자주 사용되는 필드에 권장
데이터 타입
repeated string hobbies = 4;
message Address {
string street = 1;
string city = 2;
}
Address address = 5;
- 문자열(string), 정수(int32, int64), 부동소수점(float, double), 불리언(bool) 등
- 열거형(Enum) 정의 : message 내부에 enum을 정의 가능
- 반복 필드 : repeated 키워드를 사용하여 배열이나 리스트와 같은 반복 필드를 정의
- 중첩 메시지 : message 안에 다른 message를 정의 가능
필드 옵션
-
optional: proto3에서는 기본적으로 모든 필드가 optional로 취급 -
required: proto2에서 사용. proto3에서는 사용되지 않음
주석
-
//: 한 줄 주석 -
/* */:여러 줄 주석
.proto 컴파일
src 폴더
src 폴더를 만들고 해당 위치에서 아래의 컴파일 명령을 실행합니다.
project/
├── protos/
│ └── person.proto
└── src/
└── (현재 위치)
protoc
protoc를 이용하여 .proto 파일을 컴파일해 파이썬 코드를 만들어 냅니다.
$ python -m grpc_tools.protoc -I../protos --python_out=. ../protos/person.proto
-
I../protos: proto 파일을 찾을 디렉토리를 지정 -
-python_out=.: 생성된 Python 코드를 현재 디렉토리에 출력 -
../protos/person.proto: 컴파일할 proto 파일의 경로를 지정
결과 파일
결과 파일의 이름은 원본 .proto 파일 이름에 _pb2가 추가됩니다. person.proto 파일을 이용해 person_pb2.py라는 파일이 생성됩니다.
project/
├── protos/
│ └── person.proto
└── src/
└── *person_pb2.py (결과 파일)
_pb2.py
생성된 코드는 다음과 같은 내용을 포함합니다.
- 메시지 클래스 정의
- 필드 정의 및 메타데이터
- 열거형(Enum) 정의
- 직렬화/역직렬화 메서드
- 디스크립터(Descriptor) 정보
직렬화(Serialization)
main.py 파일을 추가하고 컴파일된 파일을 통해 메시지를 바이너리 형식으로 직렬화하는 방법을 알아보겠습니다.
project/
├── protos/
│ └── person.proto
└── src/
└── person_pb2.py
└── *main.py (추가 파일)
코드
-
Person클래스는 person.proto 파일에 정의된 Person 메시지를 의미합니다. - protobuf 객체의 name 필드에 값을 할당합니다.
-
SerializeToString메시지를 바이너리 형식으로 직렬화합니다. 결과는 바이트 문자열입니다.
import person_pb2
person = person_pb2.Person()
person.name = "tiaz0128"
serialized = person.SerializeToString()
print(isinstance(serialized, bytes))
직렬화한 데이터는 바이너리 형식으로 매우 효율적이며, 네트워크 전송이나 파일 저장에 적합합니다.
역직렬화(Deserialization)
바이너리 형식으로 직렬화한 데이터를 다시 protobuf 객체로 만드는 역직렬화 방법을 알아보겠습니다.
코드
새로운 protobuf 객체를 생성하고 ParseFromString 메서드를 통해서 역직렬화 할 수 있습니다.
new_person = person_pb2.Person()
new_person.ParseFromString(serialized)
print(new_person.name)
바이트 문자열을 파싱하여 각 필드의 값을 객체의 해당 필드에 할당합니다. 이 과정에서 protobuf의 인코딩 규칙을 따라 데이터를 해석합니다.
잘못된 형식의 데이터가 입력되면 예외가 발생하며, 불완전한 데이터가 입력되어도 가능한 만큼 파싱을 시도합니다. 파싱된 필드만 설정되고, 나머지는 기본값을 유지합니다.
다른 형식으로 직렬화
JSON, 딕셔너리 같은 다른 형식으로 직렬화하고 다시 protobuf 객체로 역직렬화 할 수 있습니다. 이외에도 파일로 저장하거나 스트림 형태를 활용 할 수 있습니다.
JSON 형식
from google.protobuf.json_format import MessageToJson, Parse
json_string = MessageToJson(person)
other_person = person_pb2.Person()
Parse(json_string, other_person)
딕셔너리 형식
from google.protobuf.json_format import MessageToDict, ParseDict
dict_data = MessageToDict(person)
other_person = person_pb2.Person()
ParseDict(dict_data, other_person)
마무리
Protocol Buffers는 JSON, XML 같은 데이터를 표현하는 포맷 입니다. 문자열 기반인 다른 포맷과 달리 이진 데이터를 활용합니다.
- 크기가 작고 빠르지만, 읽거나 디버깅이 어렵다
- 스키마 파일을 작성 →
.proto - 컴파일 결과 파일을 구현에 사용 →
_pb2 - 직렬화 →
SerializeToString - 역직렬화 →
ParseFromString - 다른 포맷 직렬화, 역직렬화 지원
다음으로
gRPC에서 Protocol Buffers를 사용하기 때문에 기본적인 사용법을 이해하고 있으면 gRPC를 이해하는데 도움이 됩니다. 이제 gRPC 실전에서 gRPC의 기본적인 사용 방법에 대해서 알아보겠습니다! 😊
Let’s take a look at Protocol Buffers, which gRPC uses.
Protocol Buffers
Protocol Buffers is a binary serialization format created by Google, often shortened to protobuf. Like JSON, YAML and XML, it’s a format for representing data. The biggest difference from those familiar formats is that it represents data in binary. Here are the pros and cons of protobuf.
Pros
- Very fast serialization and deserialization
- Small data size, so network transfer is efficient
- Usable from many programming languages (Java, Python, C++, Go, etc.)
- Type checking at compile time helps prevent runtime errors
Cons
- Being binary, it’s hard for humans to read and hard to debug
- You have to learn how to write .proto files and how to use protobuf
- Hard to use directly in a web browser
- For simple data exchange, it’s overly complex compared to JSON or XML
Installing protobuf
Follow the installation guide on the gRPC site to install it. I recommend one of the three methods below. Here, we’ll install the python module.
- Install the module for your language
- Use brew
- Download a precompiled binary
Installing via apt
Always check that you have the latest version. I don’t recommend installing via apt.
# Not recommended
$ apt install protobuf-compiler
Installing the Python Module
- Python
3.7or later - pip version 9.0.1 or later
$ pip install grpcio-tools
$ python -m grpc_tools.protoc --version
Writing a .proto File
The very first step is to write a schema file called .proto. Compiling this file produces files that let each language use protobuf. Think of it as generating interface code.
Let’s write one while comparing a basic JSON data structure with its .proto equivalent.
JSON
{
"person": {
"name": "tiaz0128"
}
}
Writing the .proto
We’ll write a file called person.proto under the protos folder.
syntax = "proto3" means we’re using version 3, which is what’s mostly used. If you leave out the syntax statement, version 2 is used by default, so always include it.
syntax = "proto3";
message Person {
string name = 1;
}
message Data {
Person person = 1;
}
Writing Messages
A message is the core building block of protobuf and is used to define data structures. Let’s go over the basic syntax.
Code
syntax = "proto3";
message Person {
string name = 1;
int32 age = 2;
enum Gender {
UNKNOWN = 0;
MALE = 1;
FEMALE = 2;
}
Gender gender = 3;
repeated string hobbies = 4;
message Address {
string street = 1;
string city = 2;
}
Address address = 5;
}
Basic Structure
string name = 1;
- Fields are defined inside curly braces
{} - Starts with the message keyword, followed by the message name
- Each field is defined in the form
type name = number; - Each field is assigned a unique number (range 1-536,870,911).
- Numbers 1-15 take only one byte when encoded, so they’re recommended for frequently used fields
Data Types
repeated string hobbies = 4;
message Address {
string street = 1;
string city = 2;
}
Address address = 5;
- string, integers (int32, int64), floating point (float, double), boolean (bool), etc.
- Enum definitions : you can define an enum inside a message
- Repeated fields : use the repeated keyword to define repeated fields such as arrays or lists
- Nested messages : you can define a message inside another message
Field Options
-
optional: in proto3, all fields are treated as optional by default -
required: used in proto2. Not used in proto3
Comments
-
//: single-line comment -
/* */: multi-line comment
Compiling the .proto
The src Folder
Create a src folder and run the compile command below from there.
project/
├── protos/
│ └── person.proto
└── src/
└── (you are here)
protoc
Use protoc to compile the .proto file into Python code.
$ python -m grpc_tools.protoc -I../protos --python_out=. ../protos/person.proto
-
I../protos: specifies the directory to look for proto files in -
-python_out=.: writes the generated Python code to the current directory -
../protos/person.proto: specifies the path of the proto file to compile
Output File
The output file is named after the original .proto file with _pb2 appended. From person.proto, a file called person_pb2.py is generated.
project/
├── protos/
│ └── person.proto
└── src/
└── *person_pb2.py (output file)
_pb2.py
The generated code includes the following.
- Message class definitions
- Field definitions and metadata
- Enum definitions
- Serialization/deserialization methods
- Descriptor information
Serialization
Let’s add a main.py file and see how to serialize a message into binary format using the compiled file.
project/
├── protos/
│ └── person.proto
└── src/
└── person_pb2.py
└── *main.py (new file)
Code
- The
Personclass corresponds to the Person message defined in person.proto. - We assign a value to the name field of the protobuf object.
-
SerializeToStringserializes the message into binary format. The result is a byte string.
import person_pb2
person = person_pb2.Person()
person.name = "tiaz0128"
serialized = person.SerializeToString()
print(isinstance(serialized, bytes))
Serialized data in binary format is very efficient, and well suited to network transfer or saving to files.
Deserialization
Now let’s see how to deserialize the binary data back into a protobuf object.
Code
Create a new protobuf object and deserialize with the ParseFromString method.
new_person = person_pb2.Person()
new_person.ParseFromString(serialized)
print(new_person.name)
It parses the byte string and assigns each field’s value to the matching field of the object, interpreting the data according to protobuf’s encoding rules.
If malformed data comes in, an exception is raised; if incomplete data comes in, it still tries to parse as much as it can. Only the parsed fields are set, and the rest keep their default values.
Serializing to Other Formats
You can serialize to other formats such as JSON or dictionaries and deserialize back into a protobuf object. You can also save to files or work with streams.
JSON Format
from google.protobuf.json_format import MessageToJson, Parse
json_string = MessageToJson(person)
other_person = person_pb2.Person()
Parse(json_string, other_person)
Dictionary Format
from google.protobuf.json_format import MessageToDict, ParseDict
dict_data = MessageToDict(person)
other_person = person_pb2.Person()
ParseDict(dict_data, other_person)
Wrapping Up
Protocol Buffers is a format for representing data, like JSON or XML. Unlike those text-based formats, it uses binary data.
- Small and fast, but hard to read or debug
- Write a schema file →
.proto - Use the compiled output in your implementation →
_pb2 - Serialize →
SerializeToString - Deserialize →
ParseFromString - Supports serializing and deserializing to other formats
What’s Next
gRPC uses Protocol Buffers, so understanding the basics helps a lot in understanding gRPC. Next, in gRPC in Practice, we’ll learn the basics of using gRPC! 😊