메뉴 닫기

파이썬 Protobuf proto3 가이드, 필드 번호부터 betterproto 코드 생성까지 한 번에 정리

파이썬 Protobuf proto3 가이드, 필드 번호부터 betterproto 코드 생성까지 한 번에 정리

🧩 proto3 스키마로부터 파이썬 코드를 자동 생성하고 필드 번호, optional·repeated·map 필드, JSON 직렬화 규칙, betterproto dataclass 스타일까지 핵심만 쉽게 이해해봅니다

업무에서 서비스를 여러 언어로 나눠 개발하다 보면 데이터 구조를 어떻게 주고받을지부터 고민하게 됩니다.
REST JSON만으로 버티다가 한계가 오는 시점이 있고, 이때 자주 등장하는 해법이 바로 Protocol Buffers, 줄여서 Protobuf입니다.
바이너리 포맷이라 가볍고 빠르다는 장점도 있지만, 진짜 매력은 명확한 스키마를 작성하면 그걸 바탕으로 각 언어별 코드까지 자동으로 생성해준다는 점입니다.
이 글에서는 Protobuf의 최신 기본 문법인 proto3를 기준으로, 스키마(.proto)를 어떻게 설계하고, 필드 번호를 왜 신중하게 골라야 하며, optional / repeated / map 같은 필드 타입은 실제로 어떤 의미를 가지는지, 그리고 최종적으로 파이썬 코드에선 이게 어떻게 직렬화(바이너리와 JSON)되는지 하나씩 짚어봅니다.
특히 많은 분들이 궁금해하는 파이썬 쪽 코드 생성은 두 가지 스타일이 있습니다.
하나는 공식 protoc –python_out으로 만들어지는 전통적인 클래스 스타일이고, 다른 하나는 betterproto처럼 파이썬 dataclass 느낌으로 훨씬 읽기 좋은 클래스를 뽑아주는 방식입니다.
현업 코드 가독성, 타입 힌트, async gRPC 클라이언트까지 생각하면 betterproto를 쓰는 팀도 점점 늘고 있습니다.
이런 배경을 알아두면 “우리는 어떤 방식으로 메시지 구조를 잡을까?”를 결정할 때 훨씬 덜 헤매게 됩니다.

Protobuf를 직접 다뤄본 사람이 아니더라도, 한 번쯤은 “이 필드는 없어질 수도 있는데 그냥 비워두면 되나?” 또는 “0이라는 값이 온 건 진짜 0인지, 아니면 그냥 아무 값도 안 온 건지 어떻게 구분하지?” 같은 고민을 하게 됩니다.
proto3는 초기에 이런 ‘필드 존재 여부(presence)’를 단순화했지만, 이후 optional 키워드가 다시 공식 지원되면서(프로토버퍼 3.15 이후 정식 지원) 특정 필드가 정말로 설정된 것인지, 아니면 그냥 기본값인지까지 구분할 수 있게 됐습니다.
또한 proto3에서는 repeated 필드로 리스트를 표현하고, map<K,V> 타입으로 키-값 구조를 표현할 수 있으며, 이 구조는 파이썬 쪽에서도 자연스럽게 리스트와 딕셔너리처럼 다뤄집니다.
이 값들은 바이너리로도 직렬화할 수 있고, JSON으로도 직렬화할 수 있는데, proto3의 JSON 매핑 규칙은 각 필드 이름을 lowerCamelCase로 내보내고, map은 일반적인 JSON 오브젝트로 변환되며, repeated는 배열로 출력되는 식이라 언어 간 통신에도 유용합니다.
그리고 이런 직렬화/역직렬화 로직은 betterproto가 생성한 dataclass 스타일 코드 안에도 이미 들어가 있어서, 개발자가 따로 파서 코드를 짤 필요가 거의 없습니다.
즉, 스키마만 잘 정의하면 파이썬 쪽에서 바로 직관적인 데이터 클래스로 쓰고, 필요할 때 바이너리나 JSON으로 덤프해서 네트워크로 보내는 식의 흐름을 만들 수 있게 되는 셈입니다.

이번 글은 이런 흐름을 단계적으로 살펴볼 수 있도록 구성했습니다.
먼저 proto3 스키마에서 필드를 선언할 때 반드시 적어야 하는 필드 번호(field number)의 의미부터 정리합니다.
이 번호는 실제 전송 시에도 그대로 쓰이는 식별자라서, 배포 후에는 함부로 바꾸면 안 됩니다.
그다음, 단일 필드 / optional / repeated / map 필드가 각각 어떤 상황에서 쓰이는지와 presence 개념을 비교합니다.
또한 proto3 메시지를 파이썬 코드로 어떻게 생성하는지, 표준 방식과 betterproto 방식의 차이를 예시 중심으로 설명합니다.
마지막으로 proto3 메시지가 JSON으로 어떻게 매핑되는지도 짚어봅니다.
이 구조를 통해 “스키마 → 코드 → 직렬화”까지의 흐름을 한눈에 이어서 이해할 수 있도록 하는 게 목표입니다.



🧠 proto3 스키마에서 필드 번호는 왜 중요한가

Protocol Buffers에서 메시지를 정의할 때는 각 필드마다 숫자 ID를 붙입니다.
이걸 필드 번호(field number)라고 부르며, 예를 들면 이런 식입니다.

CODE BLOCK
syntax = "proto3";

message UserProfile {
    string user_id   = 1;
    string nickname  = 2;
    int32  age       = 3;
}

여기서 user_id의 번호는 1, nickname은 2, age는 3입니다.
겉으로 보면 단순히 “필드마다 숫자 하나씩 붙였네?” 정도로 보이지만, 실제로는 이 숫자가 네트워크 전송과 저장에서 핵심 역할을 합니다.
왜냐면 Protobuf는 전송할 때 필드 이름 대신 필드 번호를 사용해 직렬화하기 때문입니다.
즉, user_id라는 글자 문자열이 나가는 게 아니라 “필드 번호 1에 해당하는 값”이 바이너리로 실려 나갑니다.
이 구조 덕분에 전송 크기가 작고 빠릅니다.

문제는 이 번호가 바뀌면 해석이 완전히 달라질 수 있다는 점입니다.
예를 들어 운영 중인 서비스에서 user_id의 번호가 1이었는데, 나중에 “구조 좀 바꿔볼까?” 하고 nickname과 번호를 바꿔 user_id=2, nickname=1로 바꿔버리면 이미 배포된 다른 서비스들은 여전히 필드 번호 1을 user_id라고 믿고 데이터를 읽습니다.
그 결과 서로 다른 의미로 메시지를 해석하게 되고, 실제 유저 데이터가 엉뚱하게 들어가거나 타입 변환 에러가 날 수 있습니다.
즉, 필드 이름은 바뀌어도 어느 정도 괜찮을 수 있지만 필드 번호는 한 번 공개 API로 나간 순간 사실상 고정이라고 생각하는 편이 안전합니다.

그래서 proto3로 메시지를 설계할 때 많이들 하는 습관이 있습니다.
미래에 쓸 수도 있는 번호를 한두 개 비워 두는 겁니다.
예를 들어 지금은 user_id, nickname, age만 있어도 되지만 추후에 status나 profile_image_url 같은 필드를 넣고 싶을 수도 있죠.
이때 10 이상의 번호를 일부러 남겨두고, 중요한(실제로 클라이언트 간 동기화에 꼭 필요한) 필드에는 낮은 번호를 주는 식으로 전략을 세우는 경우가 많습니다.
번호는 1부터 2,147,483,647까지 쓸 수 있지만, 예약 구간이나 내부용으로 권장되지 않는 번호 범위가 있기 때문에(예: 19000~19999는 Protobuf에서 내부적으로 예약된 영역으로 간주되므로 피하는 식) 너무 아무 숫자나 쓰기보다는 팀 컨벤션을 맞춰두는 게 좋습니다.

또 한 가지 꼭 기억할 점은 필드를 삭제할 때의 처리입니다.
운영하면서 “age는 이제 안 쓰니까 지우자”라고 할 수 있죠.
그럴 때 단순히 해당 줄만 지우고 끝내면, 미래의 어느 순간 누군가가 “새로운 필드 필요하네? 비어 있는 번호 3 쓰자”라고 되살릴 가능성이 생깁니다.
이건 매우 위험합니다.
왜냐면 오래된 클라이언트는 여전히 “번호 3 = age”라고 믿고 있을 수 있기 때문입니다.
그래서 Protobuf에서는 사용 중단된 번호와 필드명을 reserved로 명시해 다시는 재사용하지 못하게 막는 패턴을 권장합니다.

CODE BLOCK
message UserProfile {
    reserved 3;
    reserved "age";

    string user_id   = 1;
    string nickname  = 2;
    // int32 age     = 3;  // 기존에 있던 필드, 이제 사용 금지
}

이렇게 선언해두면 나중에 실수로라도 3번 번호나 “age”라는 이름을 다시 쓰려고 할 때 컴파일(=코드 생성) 단계에서 바로 에러가 납니다.
눈앞의 편의 때문에 “그냥 지우자” 하고 넘어갔다가 나중에 복구 불가능한 호환성 이슈로 이어지는 걸 막아주는 안전벨트라고 보면 됩니다.
특히 모바일 앱처럼 버전 파편화가 심한 환경에서는 이게 실제로 서비스 안전성을 좌우합니다.

정리하자면 proto3에서의 필드 번호는 단순한 인덱스가 아니라 곧 메시지의 공용 언어입니다.
파이썬, 고, 자바, 노드 등 서로 다른 언어의 서비스들이 전부 같은 메시지를 이해하게 만드는 열쇠이자, 배포 이후에는 거의 바꿀 수 없는 계약 같은 존재입니다.
번호를 신중하게 할당하고, 지운 번호는 reserved로 묶어두고, 이미 퍼진 번호는 절대 재활용하지 않는 패턴을 조직 차원에서 지켜주는 것만으로도 장기 유지보수 난이도가 눈에 띄게 내려갑니다.

🧩 optional, repeated, map 필드 구조와 presence 개념 이해하기

proto3의 핵심 변화 중 하나는 “필드 존재 여부(presence)”에 대한 개념이 단순화되었다는 점입니다.
proto2 시절에는 모든 필드가 기본적으로 optional이었고, 값이 지정되지 않은 상태를 명확히 구분할 수 있었습니다.
하지만 proto3 초기에는 이 기능이 빠지면서, 기본값과 ‘미설정 상태’를 구분하기 어려워졌죠.
예를 들어 int32 타입의 필드가 있을 때, 값이 0이면 ‘정말 0인지’, 아니면 ‘아직 설정되지 않은 건지’를 알 수 없었습니다.

이 문제를 해결하기 위해 2020년 이후에는 proto3에서도 optional 키워드가 다시 도입되었습니다.
이제는 proto2처럼 특정 필드가 설정되었는지 여부를 명확히 구분할 수 있습니다.

CODE BLOCK
message UserProfile {
    string user_id = 1;
    optional int32 age = 2;
}

이렇게 정의하면 파이썬 코드에서 hasattr() 또는 message.HasField(“age”) 같은 방식으로 해당 필드가 설정되었는지를 확인할 수 있습니다.
이는 특히 ‘값이 0인 경우’를 유의미하게 해석해야 하는 로직에서 매우 중요합니다.
예를 들어 “0세”와 “나이 미입력”은 완전히 다른 의미니까요.

📚 repeated 필드와 리스트 구조

repeated 필드는 말 그대로 ‘반복되는 항목’을 표현합니다.
파이썬으로 따지면 리스트(list)에 해당하죠.
예를 들어 한 사용자가 여러 개의 이메일 주소를 가질 수 있다면 다음처럼 정의할 수 있습니다.

CODE BLOCK
message Contact {
    string name = 1;
    repeated string emails = 2;
}

이 구조는 파이썬 코드로 변환되면 list로 다뤄집니다.
즉, contact.emails.append(“user@example.com”) 같은 코드로 데이터를 추가할 수 있고, 직렬화 시에는 자동으로 배열 형태로 전송됩니다.
만약 이 필드를 JSON으로 변환하면 다음처럼 표현됩니다.

CODE BLOCK
{
  "name": "Alice",
  "emails": ["a@ex.com", "b@ex.com"]
}

repeated 필드는 빈 리스트가 기본값이므로, 값이 설정되지 않더라도 None이 아니라 빈 배열로 직렬화됩니다.
즉, 존재하지 않는 필드로 판단되지 않습니다.

🗺️ map 필드로 키-값 구조 표현하기

proto3에서는 map<key_type, value_type> 문법을 제공하여 딕셔너리(dictionary)처럼 사용할 수 있습니다.
예를 들어 사용자별 설정값을 표현할 때 다음처럼 선언할 수 있습니다.

CODE BLOCK
message Settings {
    map<string, string> preferences = 1;
}

이 구조는 파이썬 코드에서 dict로 변환되어 사용됩니다.
JSON 포맷에서도 그대로 객체(Object)로 직렬화되므로, 직관적인 구조로 언어 간 호환성을 보장합니다.
단, map의 key 타입은 반드시 기본 스칼라 타입(예: int32, string 등)이어야 하고, message 타입은 사용할 수 없습니다.

💎 핵심 포인트:
optional은 존재 여부를 구분하고, repeated는 리스트 구조를, map은 딕셔너리 구조를 표현합니다.
이 세 가지는 proto3에서 메시지를 풍부하게 만드는 기본 뼈대입니다.



🛠️ proto3 스키마로부터 파이썬 코드 생성하는 방법

proto3의 스키마(.proto 파일)는 단순한 텍스트 정의일 뿐이지만, 이를 각 언어에서 실제 사용할 수 있게 하려면 코드 생성이 필요합니다.
파이썬에서는 공식 툴체인 protoc 명령을 사용합니다.
이 명령어는 Google의 프로토콜 버퍼 컴파일러로, .proto 파일을 읽어 자동으로 파이썬 클래스를 만들어줍니다.

⚙️ protoc 기본 사용법

아래는 가장 기본적인 코드 생성 명령어입니다.

CODE BLOCK
protoc --python_out=. user.proto

이 명령을 실행하면 같은 디렉터리에 user_pb2.py 파일이 생성됩니다.
이 파일에는 proto3에서 정의한 메시지(UserProfile 등)가 파이썬 클래스로 변환되어 포함됩니다.
사용법도 간단합니다.

CODE BLOCK
from user_pb2 import UserProfile

user = UserProfile(user_id="u001", nickname="alex", age=25)
data = user.SerializeToString()
print(data)  # 바이너리 출력

parsed = UserProfile()
parsed.ParseFromString(data)
print(parsed)

이처럼 SerializeToString() 메서드는 바이너리 직렬화를 수행하고, ParseFromString()은 역직렬화를 수행합니다.
즉, 별도의 JSON 직렬화 라이브러리를 쓸 필요 없이 Protobuf 내부에서 안전하고 빠르게 변환됩니다.

📂 gRPC 서비스 코드까지 함께 생성하기

만약 proto3 스키마에 service 블록이 포함되어 있다면(예: gRPC API 정의), 단순한 메시지 파일만이 아니라 서비스 스텁(stub) 코드까지 생성할 수도 있습니다.
이때는 grpc_tools.protoc 모듈을 사용합니다.

CODE BLOCK
python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. user.proto

이 명령으로 두 개의 파일이 생성됩니다.

파일명 설명
user_pb2.py 메시지 클래스 정의
user_pb2_grpc.py gRPC 서비스 스텁 및 서버 코드

이 구조를 기반으로 서버와 클라이언트 간 통신을 쉽게 구성할 수 있습니다.
즉, 메시지와 서비스 모두를 한 스키마 파일로 관리하면서 언어별 코드를 자동으로 맞춰주는 게 proto3의 가장 큰 장점 중 하나입니다.

💡 TIP: 팀 내 여러 언어로 동시에 코드를 생성할 때는 protoc-gen-go, protoc-gen-ts 같은 언어별 플러그인을 설치해 동일한 .proto 스키마로 관리하면, API 정의를 완벽하게 일관되게 유지할 수 있습니다.

📦 JSON 직렬화 규칙과 실제 전송 형태

proto3는 기본적으로 바이너리 직렬화 포맷을 사용하지만, 최근에는 JSON 포맷 매핑도 많이 활용됩니다.
특히 REST API나 웹 브라우저 클라이언트와 연동할 때는 JSON이 훨씬 읽기 쉽고 디버깅이 편하죠.
다행히 Google 공식 protobuf 패키지에는 proto3 메시지를 JSON 문자열로 변환하는 기능이 내장돼 있습니다.

CODE BLOCK
from google.protobuf.json_format import MessageToJson, Parse

json_str = MessageToJson(user)
print(json_str)

이렇게 변환된 JSON은 proto3의 JSON 매핑 규칙에 따라 구조화됩니다.
매핑 규칙은 언어 간 일관성을 보장하기 위해 정해져 있으며, 다음의 주요 원칙을 따릅니다.

구성 요소 JSON 매핑 규칙
필드 이름 snake_case → lowerCamelCase로 변환
enum 값 이름 문자열로 표현
repeated 필드 JSON 배열로 직렬화
map 필드 JSON 객체로 매핑
기본값 미지정 필드 출력에서 생략

예를 들어 다음과 같은 proto3 메시지가 있다고 가정해봅시다.

CODE BLOCK
message UserProfile {
    string user_id = 1;
    int32 age = 2;
    repeated string tags = 3;
    map<string, string> meta = 4;
}

이 메시지를 JSON으로 직렬화하면 아래와 같이 변환됩니다.

CODE BLOCK
{
  "userId": "u001",
  "age": 25,
  "tags": ["admin", "tester"],
  "meta": {
    "country": "KR",
    "lang": "ko"
  }
}

즉, 필드 이름이 자동으로 lowerCamelCase로 바뀌고, map은 일반적인 JSON 객체로 표현됩니다.
만약 age 필드가 설정되지 않았다면 JSON에는 아예 포함되지 않습니다.
이는 “기본값 미출력” 규칙 덕분이며, 직렬화된 데이터가 훨씬 간결해집니다.

⚠️ 주의: proto3 JSON 포맷은 사람이 읽기 쉽게 설계된 보조 기능일 뿐, 공식 전송 표준은 여전히 바이너리 포맷입니다.
API 간 통신이나 gRPC 내부 전송에서는 JSON이 아닌 바이너리 직렬화를 기본으로 사용하는 것이 성능상 유리합니다.

하지만 백엔드 로그나 브라우저 디버깅 등에서는 JSON 포맷이 훨씬 직관적이기 때문에, 두 가지 포맷을 병행 지원하는 경우가 많습니다.
Google의 MessageToDict(), ParseDict() 함수도 함께 활용하면 데이터 변환 단계를 간소화할 수 있습니다.



🐍 betterproto로 뽑는 dataclass 스타일 Python 코드

기본 protoc –python_out 방식으로 생성된 코드의 가장 큰 단점은, 파이썬답지 않다는 점입니다.
클래스는 존재하지만 타입 힌트가 부족하고, 데이터 접근 방식이 직관적이지 않죠.
이 문제를 해결하기 위해 등장한 라이브러리가 바로 betterproto입니다.
이 라이브러리는 프로토콜 버퍼 정의를 기반으로 dataclass 스타일의 깔끔한 파이썬 코드를 생성해줍니다.
더불어 async gRPC를 지원하는 현대적인 구조라, 타입 안정성과 코드 가독성 면에서 훨씬 우수합니다.

⚙️ betterproto 설치 및 코드 생성

먼저 betterproto를 설치합니다.

CODE BLOCK
pip install betterproto

그 다음, betterproto 전용 플러그인을 protoc에 연결하여 코드를 생성합니다.

CODE BLOCK
protoc -I . --python_betterproto_out=. user.proto

생성된 파이썬 파일은 다음과 같이 dataclass 구조를 따릅니다.

CODE BLOCK
from dataclasses import dataclass
import betterproto

@dataclass(eq=False, repr=False)
class UserProfile(betterproto.Message):
    user_id: str = betterproto.string_field(1)
    nickname: str = betterproto.string_field(2)
    age: int = betterproto.int32_field(3)

이제 사용자는 단순히 dataclass처럼 객체를 만들고, to_json()이나 from_json() 메서드로 쉽게 직렬화할 수 있습니다.

CODE BLOCK
user = UserProfile(user_id="u001", nickname="alex", age=25)
print(user.to_json(indent=2))

출력 결과는 JSON 직렬화 규칙을 그대로 따르며, 타입 안정성까지 유지됩니다.
즉, Protobuf의 효율성과 파이썬의 직관적인 데이터 모델링을 한 번에 잡을 수 있는 구조입니다.

🚀 betterproto의 장점 요약

  • 🧩dataclass 기반 구조로 가독성타입 안정성 강화
  • ⚙️비동기 async gRPC 클라이언트 자동 생성 지원
  • 📦내장 to_json() / from_json() 기능으로 직렬화 편의성 향상
  • 🔍IDE 자동완성, 타입체커(Mypy) 완벽 호환

💎 핵심 포인트:
betterproto는 proto3의 장점을 그대로 살리면서도, 파이썬스러운 코드를 원하는 개발자에게 최적화된 선택입니다.
단순한 메시지 직렬화 이상으로, 유지보수성과 협업 효율성을 높여주는 강력한 도구입니다.

자주 묻는 질문 (FAQ)

proto2와 proto3는 어떻게 다르나요?
proto2는 모든 필드가 기본적으로 optional이었고, presence 체크가 항상 가능했습니다.
proto3는 단순화를 위해 기본값 중심의 설계를 도입했지만, 이후 optional이 다시 도입되며 유연성이 커졌습니다.
필드 번호를 나중에 바꿔도 되나요?
절대 바꾸면 안 됩니다.
필드 번호는 전송 시 메시지 식별자 역할을 하기 때문에, 기존 클라이언트와 서버 간 데이터 불일치가 발생할 수 있습니다.
삭제 시에는 reserved 구문을 반드시 사용하세요.
optional 필드와 기본값 0의 차이는 무엇인가요?
optional은 “필드가 존재하지 않는다”는 상태를 구분할 수 있습니다.
즉, 값이 0인 것과 아예 지정되지 않은 상태를 구별할 수 있다는 점이 핵심입니다.
map 필드는 어떤 데이터 구조로 직렬화되나요?
map은 JSON 직렬화 시 일반 객체(Object)로 표현되며, 파이썬에서는 dict 타입으로 변환됩니다.
단, key는 스칼라 타입만 사용할 수 있습니다.
repeated 필드는 빈 배열로 직렬화되나요?
네, repeated 필드는 값이 없을 경우에도 None이 아닌 빈 배열([])로 직렬화됩니다.
따라서 클라이언트는 항상 반복 가능한 리스트 형태로 데이터를 받을 수 있습니다.
proto3 JSON 직렬화 시 필드가 빠지는 이유는 뭔가요?
proto3는 기본값(0, “”, false 등)을 갖는 필드를 JSON 직렬화 시 생략합니다.
이는 전송 크기를 줄이고 불필요한 데이터를 제거하기 위한 설계입니다.
betterproto로 생성한 코드는 gRPC와 호환되나요?
네, betterproto는 async gRPC 클라이언트와 서버 코드를 자동으로 생성할 수 있습니다.
기존 gRPC-Python보다 더 현대적인 비동기 구조를 제공합니다.
proto3 파일의 확장자는 꼭 .proto여야 하나요?
네, .proto 확장자를 사용해야 protoc 컴파일러가 스키마 파일로 인식합니다.
다른 확장자를 사용하면 코드 생성이 실패합니다.

📘 proto3와 betterproto로 효율적인 데이터 직렬화 완성하기

파이썬에서 Protobuf를 제대로 이해하면, 단순한 데이터 직렬화를 넘어서 서비스 간 통신의 안정성과 확장성을 동시에 확보할 수 있습니다.
proto3의 스키마 정의를 중심으로 필드 번호 관리, optional·repeated·map 필드의 구조적 의미, 그리고 JSON 매핑 규칙까지 살펴보면 데이터 모델링이 한층 견고해집니다.
특히 betterproto를 활용하면 기존 protobuf 코드보다 훨씬 직관적인 dataclass 기반 구조를 쓸 수 있어, 타입 안정성과 유지보수성이 크게 향상됩니다.
이 조합은 API 통신, gRPC 마이크로서비스, IoT 데이터 전송 등 다양한 환경에서 동일하게 활용될 만큼 강력합니다.

결국 핵심은 명확한 스키마 설계와 일관된 코드 생성 프로세스입니다.
필드 번호를 신중히 관리하고, optional 필드로 데이터 존재 여부를 명확히 구분하며, JSON 매핑 규칙을 정확히 이해하면 언어와 플랫폼이 달라도 완벽히 호환되는 데이터 구조를 유지할 수 있습니다.
그리고 그 위에 betterproto의 dataclass 스타일을 얹으면, 파이썬 개발 환경에서도 modern하고 읽기 좋은 코드로 프로토콜 버퍼를 다룰 수 있습니다.


🏷️ 관련 태그 : protobuf, proto3, betterproto, 파이썬데이터직렬화, gRPC, 데이터모델링, JSON매핑, 필드번호, optional필드, dataclass