syntax = "proto3"; // 파일의 첫 번째 줄은 `proto3` 구문을 사용하도록 지정
message Product { // 메시지에 포함할 각 데이터에 하나씩, 필드 (이름/값 쌍)을 지정
int32 id = 1;
string name = 2;
}
자료형
double
, float
, int32
, int64
, uint32
, uint64
sint32
, sint64
(s: singed, 음수 연산에 더 효율적)fixed32
, fixed64
(고정길이 인코딩)sfixed32
, sfixed64
bool
string
bytes
enums 활용 예
enum Corpus {
CORPUS_UNSPECIFIED = 0;
CORPUS_UNIVERSAL = 1;
CORPUS_WEB = 2;
CORPUS_IMAGES = 3;
CORPUS_LOCAL = 4;
CORPUS_NEWS = 5;
CORPUS_PRODUCTS = 6;
CORPUS_VIDEO = 7;
}
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
Corpus corpus = 4;
}
중첩 자료형
message type 의 필드 자료형으로 다른 message type 을 지정할 수 있음
중첩 자료형 예 1
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
message SomeOtherMessage {
SearchResponse.Result result = 1;
}
첫 5개 bit에 필드 key (순서) 입력
그 뒤 3개 bit 에 필드 자료형에 맞는 와이어 유형 입력
(공식문서에는 첫번째 byte 에서 마지막 3개 bit 에 와이어 유형 값을 입력하고, 나머지 bit 에 필드 key 를 입력한다고 작성됨)
와이어 유형
그 다음 bytes 에 입력되는 값은 와이어 유형에 따라 다름 (아래 내용은 필자의 개인 분석에 근거함, 내용 불확실)
더 살펴볼 내용
$ sudo apt update -y && sudo apt install -y protobuf-compiler
$ protoc --version
libprotoc 3.12.4
$ rm -rf ~/myproj && mkdir ~/myproj && cd ~/myproj
$ vi product.proto
syntax = "proto3";
message Product {
int32 id = 1;
string name = 2;
bool display = 3;
}
$ mkdir ~/myproj/mypy && cd ~/myproj/mypy
$ python3 -m venv venv # 가상환경 생성
$ source venv/bin/activate # 가상환경 접속
(venv) $ pip install --upgrade pip # pip 업데이트
(venv) $
(venv) $ pip install grpcio grpcio-tools
(venv) $ mkdir ~/myproj/mypy/product
(venv) $ python -m grpc_tools.protoc \
-I=$HOME/myproj \
--python_out=$HOME/myproj/mypy/product \
--pyi_out=$HOME/myproj/mypy/product \
--grpc_python_out=$HOME/myproj/mypy/product \
product.proto
~/myproj/mypy/product
디렉터리에 grpc 관련 코드 파일이 생성됨(venv) $ ls ~/myproj/mypy/product
product_pb2.py product_pb2.pyi product_pb2_grpc.py
(venv) $ vi ~/myproj/mypy/main.py
from product.product_pb2 import Product
# 테스트용 object 생성
p1: Product = Product(
id=1,
name="가방",
display=True,
)
# object 를 bytes 로 직렬화
protobuf_bytes: bytes = p1.SerializePartialToString()
# 직렬화한 bytes 의 길이와 내용 확인
print("byte 길이:", len(protobuf_bytes))
print("byte 내용:", protobuf_bytes)
# 직렬화한 bytes 의 내용을 파일에 저장
with open("../bytes_from_python", "wb") as f:
f.write(protobuf_bytes)
# 파일에서 bytes 의 내용을 읽어옴
protobuf_bytes_read_from_file: bytes = bytes()
with open("../bytes_from_python", "rb") as f:
protobuf_bytes_read_from_file = f.read()
# 읽어온 bytes 의 내용을 새 object 에 할당
p2: Product = Product()
p2.ParseFromString(protobuf_bytes_read_from_file)
# 새로 할당한 object 의 내용 확인
print(p2)
(venv) $ python ~/myproj/mypy/main.py
byte 길이: 12
byte 내용: b'\x08\x01\x12\x06\xea\xb0\x80\xeb\xb0\xa9\x18\x01'
id: 1
name: "가방"
display: true
$ xxd -b ~/myproj/bytes_from_python
00000000: 00001000 00000001 00010010 00000110 11101010 10110000 ......
00000006: 10000000 11101011 10110000 10101001 00011000 00000001 ......
참고자료
폴더 및 가상환경 구성
$ mkdir ~/myproj/myjs && cd ~/myproj/myjs
$ npm init -d # npm 환경 구성 (~/myproj/myjs 디렉터리에서 수행)
$ npm install grpc-tools google-protobuf @grpc/grpc-js
$ $(npm bin)/grpc_tools_node_protoc \
--proto_path=$HOME/myproj \
--js_out=import_style=commonjs,binary:$HOME/myproj/myjs/product \
--grpc_out=grpc_js:$HOME/myproj/myjs/product \
product.proto
~/myproj/myjs/product
디렉터리에 grpc 관련 코드 파일이 생성됨$ ls ~/myproj/myjs/product
product_grpc_pb.js product_pb.js
protoc-gen-go 설치
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
$ export PATH="$PATH:$(go env GOPATH)/bin"
$ mkdir ~/myproj/mygo && cd ~/myproj/mygo
$ go mod init mygo
(venv) $ mkdir ~/myproj/mygo/product
proto 파일 수정
option go_package="product/pb";
문장 추가syntax = "proto3";
option go_package="product/pb";
message Product {
int32 id = 1;
string name = 2;
bool display = 3;
}
$ protoc -I $HOME/myproj \
--go_out=. \
--go-grpc_out=. \
product.proto
~/myproj/mygo/product/pb
디렉터리에 grpc 관련 코드 파일이 생성됨$ ls ~/myproj/mygo/product/pb
product.pb.go
$ go mod tidy
$ vi ~/myproj/mygo/main.go
package main
import (
"log"
"mygo/product/pb"
"os"
"google.golang.org/protobuf/proto"
)
func main() {
log.SetFlags(0)
// 테스트용 object 생성
p1 := &pb.Product{
Id: 1,
Name: "가방",
Display: true,
}
// object 를 bytes 로 직렬화
pMessage := p1.ProtoReflect().Interface()
protoBytes, err := proto.Marshal(pMessage)
if err != nil {
log.Fatal(err.Error())
}
// 직렬화한 bytes 의 길이와 내용 확인
log.Println("byte 길이:", len(protoBytes))
log.Println("byte 내용:", protoBytes)
// 직렬화한 bytes 의 내용을 파일에 저장
err = os.WriteFile("../bytes_from_go", protoBytes, os.ModePerm)
if err != nil {
log.Fatal(err.Error())
}
// 파일에서 bytes 의 내용을 읽어옴
protobufBytesReadFromFile, err := os.ReadFile("../bytes_from_go")
if err != nil {
log.Fatal(err.Error())
}
// 읽어온 bytes 의 내용을 새 object 에 할당
p2 := new(pb.Product)
if err := proto.Unmarshal(protobufBytesReadFromFile, p2); err != nil {
log.Fatal(err.Error())
}
// 새로 할당한 object 의 내용 확인
log.Println(p2)
}
byte 길이: 12
byte 내용: [8 1 18 6 234 176 128 235 176 169 24 1]
id:1 name:"가방" display:true
$ xxd -b ~/myproj/bytes_from_go
00000000: 00001000 00000001 00010010 00000110 11101010 10110000 ......
00000006: 10000000 11101011 10110000 10101001 00011000 00000001 ......
(venv) $ vi ~/myproj/mypy/main.py
from product.product_pb2 import Product
# 테스트용 object 생성
p1: Product = Product(
id=1,
name="가방",
display=True,
)
# object 를 bytes 로 직렬화
protobuf_bytes: bytes = p1.SerializePartialToString()
# 직렬화한 bytes 의 길이와 내용 확인
print("byte 길이:", len(protobuf_bytes))
print("byte 내용:", protobuf_bytes)
# 직렬화한 bytes 의 내용을 파일에 저장
with open("../bytes_from_python", "wb") as f:
f.write(protobuf_bytes)
# 파일에서 bytes 의 내용을 읽어옴
protobuf_bytes_read_from_file: bytes = bytes()
with open("../bytes_from_go", "rb") as f:
protobuf_bytes_read_from_file = f.read()
# 읽어온 bytes 의 내용을 새 object 에 할당
p2: Product = Product()
p2.ParseFromString(protobuf_bytes_read_from_file)
# 새로 할당한 object 의 내용 확인
print(p2)
# python 이 생성한 바이트와 go 가 생성한 바이트 비교
print("protobuf_bytes == protobuf_bytes_read_from_file:", protobuf_bytes == protobuf_bytes_read_from_file)
$ python ~/myproj/mypy/main.py
byte 길이: 12
byte 내용: b'\x08\x01\x12\x06\xea\xb0\x80\xeb\xb0\xa9\x18\x01'
id: 1
name: "가방"
display: true
protobuf_bytes == protobuf_bytes_read_from_file: True
package main
import (
"log"
"mygo/product/pb"
"os"
"google.golang.org/protobuf/proto"
)
func main() {
log.SetFlags(0)
// 테스트용 object 생성
p1 := &pb.Product{
Id: 1,
Name: "가방",
Display: true,
}
// object 를 bytes 로 직렬화
pMessage := p1.ProtoReflect().Interface()
protoBytes, err := proto.Marshal(pMessage)
if err != nil {
log.Fatal(err.Error())
}
// 직렬화한 bytes 의 길이와 내용 확인
log.Println("byte 길이:", len(protoBytes))
log.Println("byte 내용:", protoBytes)
// 직렬화한 bytes 의 내용을 파일에 저장
err = os.WriteFile("../bytes_from_go", protoBytes, os.ModePerm)
if err != nil {
log.Fatal(err.Error())
}
// 파일에서 bytes 의 내용을 읽어옴
protobufBytesReadFromFile, err := os.ReadFile("../bytes_from_python")
if err != nil {
log.Fatal(err.Error())
}
// 읽어온 bytes 의 내용을 새 object 에 할당
p2 := new(pb.Product)
if err := proto.Unmarshal(protobufBytesReadFromFile, p2); err != nil {
log.Fatal(err.Error())
}
// 새로 할당한 object 의 내용 확인
log.Println(p2)
// python 이 생성한 바이트와 go 가 생성한 바이트 비교
log.Println("string(protoBytes) == string(protobufBytesReadFromFile):", string(protoBytes) == string(protobufBytesReadFromFile))
}
$ go run main.go
byte 길이: 12
byte 내용: [8 1 18 6 234 176 128 235 176 169 24 1]
id:1 name:"가방" display:true
string(protoBytes) == string(protobufBytesReadFromFile): true
syntax = "proto3";
option go_package="product/pb";
message Product {
int32 id = 1;
string name = 2;
bool display = 3;
}
// 필드 이름이 변경된 경우
message ProductFieldNameChanged {
int32 id = 1;
string title = 2;
bool display = 3;
}
// 필드가 제거된 경우
message ProductFieldDeleted {
int32 id = 1;
// string name = 2;
bool display = 3;
}
// 필드가 추가된 경우
message ProductFieldAdded {
int32 id = 1;
string name = 2;
bool display = 3;
float discount_rate = 4;
}
// 필드가 추가, 제거된 경우
message ProductFieldAddedAndDeleted {
int32 id = 1;
// string name = 2;
bool display = 3;
float discount_rate = 4;
}
$ cd ~/myproj/mygo && protoc -I $HOME/myproj --go_out=. --go-grpc_out=. product.proto
package main
import (
"log"
"mygo/product/pb"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
)
type IProtoStruct interface {
ProtoReflect() protoreflect.Message
}
func getProtoBytes(p IProtoStruct) ([]byte, error) {
return proto.Marshal(p.ProtoReflect().Interface())
}
func main() {
log.SetFlags(0)
// 변경 전 message type 의 bytes
pBytes, err := getProtoBytes(&pb.Product{
Id: 1,
Name: "가방",
Display: true,
})
if err != nil {
log.Fatal(err.Error())
}
// 필드 이름이 바뀐 message type 의 bytes
pFieldNameChangedBytes, err := getProtoBytes(&pb.ProductFieldNameChanged{
Id: 2,
Title: "상의",
Display: true,
})
if err != nil {
log.Fatal(err.Error())
}
// 필드가 제거된 message type 의 bytes
pFieldDeletedBytes, err := getProtoBytes(&pb.ProductFieldDeleted{
Id: 3,
Display: true,
})
if err != nil {
log.Fatal(err.Error())
}
// 필드가 추가된 message type 의 bytes
pFieldAddedBytes, err := getProtoBytes(&pb.ProductFieldAdded{
Id: 4,
Name: "모자",
Display: true,
DiscountRate: 0.1,
})
if err != nil {
log.Fatal(err.Error())
}
// 필드가 추가, 제거된 message type 의 bytes
pFieldAddedAndDeletedBytes, err := getProtoBytes(&pb.ProductFieldAddedAndDeleted{
Id: 5,
Display: true,
DiscountRate: 0.2,
})
if err != nil {
log.Fatal(err.Error())
}
// 다음 상황을 가정한 테스트
// proto 파일이 수정 된 후
// 서버는 수정된 내용이 코드에 반영됨
// 클라이언트는 수정된 내용이 코드에 반영되지 않았음
// 클라이언트가 수정 전 형식으로 bytes 를 보내올 때
// 서버가 수신한 bytes 를 변환할 수 있는가
p1 := new(pb.ProductFieldNameChanged)
err = proto.Unmarshal(pBytes, p1)
if err != nil {
log.Println("pBytes 는 새로운 message type ProductFieldNameChanged 과 호환되지 않음")
} else {
log.Println("pBytes 는 새로운 message type ProductFieldNameChanged 과 호환됨")
log.Println(p1)
}
p2 := new(pb.ProductFieldDeleted)
err = proto.Unmarshal(pBytes, p2)
if err != nil {
log.Println("pBytes 는 새로운 message type ProductFieldDeleted 과 호환되지 않음")
} else {
log.Println("pBytes 는 새로운 message type ProductFieldDeleted 과 호환됨")
log.Println(p2)
}
p3 := new(pb.ProductFieldAdded)
err = proto.Unmarshal(pBytes, p3)
if err != nil {
log.Println("pBytes 는 새로운 message type ProductFieldAdded 과 호환되지 않음")
} else {
log.Println("pBytes 는 새로운 message type ProductFieldAdded 과 호환됨")
log.Println(p3)
}
p4 := new(pb.ProductFieldAddedAndDeleted)
err = proto.Unmarshal(pBytes, p4)
if err != nil {
log.Println("pBytes 는 새로운 message type ProductFieldAddedAndDeleted 과 호환되지 않음")
} else {
log.Println("pBytes 는 새로운 message type ProductFieldAddedAndDeleted 과 호환됨")
log.Println(p4)
}
// 다음 상황을 가정한 테스트
// proto 파일이 수정 된 후
// 서버는 수정된 내용이 코드에 반영되지 않았음
// 클라이언트는 수정된 내용이 코드에 반영됨
// 클라이언트가 수정된 형식으로 bytes 를 보내올 때
// 서버가 수신한 bytes 를 변환할 수 있는가
p5 := new(pb.Product)
err = proto.Unmarshal(pFieldNameChangedBytes, p5)
if err != nil {
log.Println("pFieldNameChangedBytes 는 이전 message type 과 호환되지 않음")
} else {
log.Println("pFieldNameChangedBytes 는 이전 message type 과 호환됨")
log.Println(p5)
}
p6 := new(pb.Product)
err = proto.Unmarshal(pFieldDeletedBytes, p6)
if err != nil {
log.Println("pFieldDeletedBytes 는 이전 message type 과 호환되지 않음")
} else {
log.Println("pFieldDeletedBytes 는 이전 message type 과 호환됨")
log.Println(p6)
}
p7 := new(pb.Product)
err = proto.Unmarshal(pFieldAddedBytes, p7)
if err != nil {
log.Println("pFieldAddedBytes 는 이전 message type 과 호환되지 않음")
} else {
log.Println("pFieldAddedBytes 는 이전 message type 과 호환됨")
log.Println(p7)
}
p8 := new(pb.Product)
err = proto.Unmarshal(pFieldAddedAndDeletedBytes, p8)
if err != nil {
log.Println("pFieldAddedAndDeletedBytes 는 이전 message type 과 호환되지 않음")
} else {
log.Println("pFieldAddedAndDeletedBytes 는 이전 message type 과 호환됨")
log.Println(p8)
}
}
$ go run main.go
pBytes 는 새로운 message type ProductFieldNameChanged 과 호환됨
id:1 title:"가방" display:true
pBytes 는 새로운 message type ProductFieldDeleted 과 호환됨
id:1 display:true 2:"가방"
pBytes 는 새로운 message type ProductFieldAdded 과 호환됨
id:1 name:"가방" display:true
pBytes 는 새로운 message type ProductFieldAddedAndDeleted 과 호환됨
id:1 display:true 2:"가방"
pFieldNameChangedBytes 는 이전 message type 과 호환됨
id:2 name:"상의" display:true
pFieldDeletedBytes 는 이전 message type 과 호환됨
id:3 display:true
pFieldAddedBytes 는 이전 message type 과 호환됨
id:4 name:"모자" display:true 4:0x3dcccccd
pFieldAddedAndDeletedBytes 는 이전 message type 과 호환됨
id:5 display:true 4:0x3e4ccccd
from product.product_pb2 import Product
from product.product_pb2 import ProductFieldNameChanged, ProductFieldDeleted, ProductFieldAdded, ProductFieldAddedAndDeleted
from google.protobuf.message import DecodeError
# 변경 전 message type 의 bytes
p_bytes: bytes = Product(
id=1,
name="가방",
display=True,
).SerializePartialToString()
# 필드 이름이 바뀐 message type 의 bytes
p_field_name_changed_bytes: bytes = ProductFieldNameChanged(
id=2,
title="상의",
display=True,
).SerializePartialToString()
# 필드가 제거된 message type 의 bytes
p_field_deleted_bytes: bytes = ProductFieldDeleted(
id=3,
display=True,
).SerializePartialToString()
# 필드가 추가된 message type 의 bytes
p_field_added_bytes: bytes = ProductFieldAdded(
id=4,
name="모자",
display=True,
discount_rate=0.1,
).SerializePartialToString()
# 필드가 추가, 제거된 message type 의 bytes
p_field_added_and_deleted: bytes = ProductFieldAddedAndDeleted(
id=5,
display=True,
discount_rate=0.2,
).SerializePartialToString()
# 다음 상황을 가정한 테스트
# proto 파일이 수정 된 후
# 서버는 수정된 내용이 코드에 반영됨
# 클라이언트는 수정된 내용이 코드에 반영되지 않았음
# 클라이언트가 수정 전 형식으로 bytes 를 보내올 때
# 서버가 수신한 bytes 를 변환할 수 있는가
p1: ProductFieldNameChanged = ProductFieldNameChanged()
try:
p1.ParseFromString(p_bytes)
except DecodeError as e:
print("p_bytes 는 새로운 message type ProductFieldNameChanged 과 호환되지 않음")
except Exception as e:
print(e)
else:
print("p_bytes 는 새로운 message type ProductFieldNameChanged 과 호환됨")
print(p1)
p2: ProductFieldDeleted = ProductFieldDeleted()
try:
p2.ParseFromString(p_bytes)
except DecodeError as e:
print("p_bytes 는 새로운 message type ProductFieldNameChanged 과 호환되지 않음")
except Exception as e:
print(e)
else:
print("p_bytes 는 새로운 message type ProductFieldNameChanged 과 호환됨")
print(p2)
p3: ProductFieldAdded = ProductFieldAdded()
try:
p3.ParseFromString(p_bytes)
except DecodeError as e:
print("p_bytes 는 새로운 message type ProductFieldAdded 과 호환되지 않음")
except Exception as e:
print(e)
else:
print("p_bytes 는 새로운 message type ProductFieldAdded 과 호환됨")
print(p3)
p4: ProductFieldAddedAndDeleted = ProductFieldAddedAndDeleted()
try:
p4.ParseFromString(p_bytes)
except DecodeError as e:
print("p_bytes 는 새로운 message type ProductFieldAddedAndDeleted 과 호환되지 않음")
except Exception as e:
print(e)
else:
print("p_bytes 는 새로운 message type ProductFieldAddedAndDeleted 과 호환됨")
print(p4)
# 다음 상황을 가정한 테스트
# proto 파일이 수정 된 후
# 서버는 수정된 내용이 코드에 반영되지 않았음
# 클라이언트는 수정된 내용이 코드에 반영됨
# 클라이언트가 수정된 형식으로 bytes 를 보내올 때
# 서버가 수신한 bytes 를 변환할 수 있는가
p5: Product = Product()
try:
p5.ParseFromString(p_field_name_changed_bytes)
except DecodeError as e:
print("p_field_name_changed_bytes 는 이전 message type 과 호환되지 않음")
except Exception as e:
print(e)
else:
print("p_field_name_changed_bytes 는 이전 message type 과 호환됨")
print(p5)
p6: Product = Product()
try:
p6.ParseFromString(p_field_deleted_bytes)
except DecodeError as e:
print("p_field_deleted_bytes 는 이전 message type 과 호환되지 않음")
except Exception as e:
print(e)
else:
print("p_field_deleted_bytes 는 이전 message type 과 호환됨")
print(p6)
p7: Product = Product()
try:
p7.ParseFromString(p_field_added_bytes)
except DecodeError as e:
print("p_field_added_bytes 는 이전 message type 과 호환되지 않음")
except Exception as e:
print(e)
else:
print("p_field_added_bytes 는 이전 message type 과 호환됨")
print(p7)
p8: Product = Product()
try:
p8.ParseFromString(p_field_added_and_deleted)
except DecodeError as e:
print("p_field_added_and_deleted 는 이전 message type 과 호환되지 않음")
except Exception as e:
print(e)
else:
print("p_field_added_and_deleted 는 이전 message type 과 호환됨")
print(p8)
(venv) $ python main.py
p_bytes 는 새로운 message type ProductFieldNameChanged 과 호환됨
id: 1
title: "가방"
display: true
p_bytes 는 새로운 message type ProductFieldNameChanged 과 호환됨
id: 1
display: true
p_bytes 는 새로운 message type ProductFieldAdded 과 호환됨
id: 1
name: "가방"
display: true
p_bytes 는 새로운 message type ProductFieldAddedAndDeleted 과 호환됨
id: 1
display: true
p_field_name_changed_bytes 는 이전 message type 과 호환됨
id: 2
name: "상의"
display: true
p_field_deleted_bytes 는 이전 message type 과 호환됨
id: 3
display: true
p_field_added_bytes 는 이전 message type 과 호환됨
id: 4
name: "모자"
display: true
p_field_added_and_deleted 는 이전 message type 과 호환됨
id: 5
display: true