proto 파일
고친 사람 github-actions[bot]
proto 파일은 서비스끼리 주고받을 데이터의 모양을 정해 둡니다. 양쪽 서비스가 이 파일 하나를 나눠 갖습니다. 각자 이 파일에서 자기 언어의 코드를 뽑아 씁니다. 프로토콜 버퍼는 데이터를 작은 바이트열로 바꿔 보내는 방식이고, 그 바이트는 이 파일이 있어야 읽힙니다.
쉽고 빠른 이해
proto 파일은 주고받을 데이터에 어떤 칸이 있고 칸마다 무슨 값이 드는지 적어 두는 파일입니다. 사용자 데이터라면 이름 칸과 나이 칸이 있다고 적습니다. 이름에는 글자가, 나이에는 정수가 든다고도 적습니다.
프로토콜 버퍼는 데이터를 작은 바이트열로 바꿔 보내는 방식입니다. 이 방식은 바이트에 칸 이름을 싣지 않고 칸마다 정한 번호만 싣습니다. 번호가 무슨 칸인지는 이 파일에만 적혀 있습니다. 그래서 보내는 쪽과 받는 쪽이 같은 파일을 가져야 합니다.
어떻게 도나:
- 칸마다 타입 · 이름 · 번호를 파일에 적습니다
- 컴파일러가 이 파일을 읽어 자바 · 파이썬 같은 언어별 코드를 만듭니다
- 서비스들은 그 코드로 데이터를 바이트로 바꾸고 되돌립니다
대가도 있습니다. 파일을 고칠 때 번호를 바꾸면 옛 데이터가 엉뚱한 칸으로 읽힙니다. 서비스가 늘면 한 파일을 여럿이 어떻게 나눠 가질지도 정해야 합니다.
주로 서비스끼리 안쪽에서 주고받는 호출에 씁니다. 브라우저나 바깥 회사처럼 받는 쪽이 이 파일을 가질 수 없는 공개 응답에는 JSON(JavaScript Object Notation, 자바스크립트 객체 표기법)을 씁니다.
상세
이 절은 사용자 정보를 주고받는 user.proto 파일 하나를 위에서부터 읽어 내려갑니다. 맨 위의 머리말에서 시작해 메시지 · 열거형 · 서비스 정의를 한 덩이씩 봅니다.
그다음 이 파일이 코드가 되는 과정을 봅니다. 여러 서비스가 한 파일을 나눠 갖는 방법과 파일을 고칠 때 지키는 규칙도 봅니다. 끝으로 이 파일을 쓰는 때와 안 쓰는 때를 가릅니다.
이름과 하는 일
이름의 proto 는 프로토콜 버퍼(Protocol Buffers)에서 왔습니다. 프로토콜 버퍼는 프로그램끼리 주고받는 데이터를 작은 바이트열로 바꾸는 포맷입니다. 이 포맷이 쓰는 설계도를 적은 파일이 proto 파일입니다. 확장자는 .proto 입니다.
데이터의 설계도는 스키마라고 부릅니다. 데이터에 어떤 칸이 있는지, 칸마다 어떤 타입의 값이 드는지를 적은 것입니다. proto 파일은 이 스키마를 사람이 읽고 쓰는 글자로 적습니다. 파일 자체는 평범한 텍스트 파일입니다.
이 파일이 없으면 생기는 일
프로토콜 버퍼는 바이트에 칸 이름을 싣지 않습니다. 칸마다 정해 둔 번호만 싣습니다. 그래서 받은 바이트에는 「1번 칸에 유미, 2번 칸에 3」 같은 정보만 남습니다.
1번이 이름이고 2번이 나이라는 것은 proto 파일에만 적혀 있습니다. 받는 쪽이 이 파일을 모르면 바이트를 받아도 읽을 방법이 없습니다. 보내는 쪽과 받는 쪽이 같은 파일을 나눠 가져야 하는 까닭입니다.
두 쪽이 함께 지키기로 한 약속을 계약이라고 부릅니다. proto 파일은 서비스 사이의 계약을 적은 문서입니다. 한쪽이 파일을 바꾸면 다른 쪽도 그 변경을 알아야 합니다.
파일을 이루는 두 덩이
proto 파일은 크게 두 덩이로 나뉩니다. 위쪽 머리말은 파일 전체에 걸리는 설정을 적습니다. 아래쪽 정의는 주고받을 데이터와 부를 수 있는 함수를 적습니다.
flowchart TD
F["proto 파일"] --> H
F --> D
subgraph H["머리말"]
H1["syntax · 문법 판"]
H2["package · 이름 묶음"]
H3["import · 다른 파일 불러오기"]
H4["option · 코드 생성 설정"]
end
subgraph D["정의"]
M["message · 데이터 모양"] --> MF["필드"]
E["enum · 정해진 값 목록"] --> EV["값"]
S["service · 부를 함수 묶음"] --> R["rpc · 원격으로 부르는 함수"]
end
아래 소절들이 이 그림을 위에서부터 한 덩이씩 풉니다.
머리말 네 줄
머리말은 대개 이렇게 생겼습니다. 줄마다 키워드 하나로 시작합니다.
syntax = "proto3";
package shop.user;
import "google/protobuf/timestamp.proto";
option java_package = "com.example.shop.user";
네 줄이 하는 일은 이렇습니다. 표에 나오는 메시지는 주고받는 데이터 한 단위이고, 아래 「메시지와 필드」에서 따로 봅니다.
| 줄 | 하는 일 |
|---|---|
syntax |
이 파일이 따르는 문법 판을 밝힌다. proto2 와 proto3 가 있다 |
package |
이름 묶음을 정한다. 다른 파일의 같은 이름과 안 겹치게 한다 |
import |
다른 파일을 불러온다. 그 파일에 적힌 메시지를 이 파일에서 쓴다 |
option |
코드 생성 설정을 준다. 언어마다 따로 붙는 설정이 많다 |
syntax 줄을 빼면 컴파일러는 옛 판인 proto2 로 읽습니다. proto3 는 proto2 를 더 단순하게 다듬은 새 판입니다. 새로 쓰는 파일은 대개 proto3 로 적습니다.
패키지 이름은 메시지 이름 앞에 붙어 온전한 이름이 됩니다. 아래에서 볼 User 메시지는 다른 파일에서 shop.user.User 로 부릅니다. 두 팀이 똑같이 User 라는 메시지를 만들어도 패키지가 다르면 안 부딪힙니다.
google/protobuf/timestamp.proto 는 프로토콜 버퍼가 함께 내주는 파일입니다. 시각을 담는 Timestamp 메시지가 들어 있습니다. 직접 만든 다른 proto 파일도 같은 방법으로 불러옵니다.
java_package 는 만들어질 자바 코드가 들어갈 패키지 이름입니다. 이 줄이 없으면 위의 package 이름을 씁니다.
메시지와 필드
메시지는 주고받는 데이터 한 단위입니다. 자바의 클래스 하나에 해당합니다. message 뒤에 이름을 쓰고 중괄호 안에 칸을 적습니다.
message User {
string name = 1;
int32 age = 2;
repeated string tags = 3;
google.protobuf.Timestamp joined_at = 4;
Status status = 5;
}
중괄호 안의 한 줄이 필드 하나입니다. 필드는 메시지 안의 칸입니다. 한 줄은 「타입 이름 = 번호」 순서로 적습니다.
= 1 은 값을 넣는 식이 아닙니다. 필드마다 붙이는 필드 번호입니다. 바이트에는 필드 이름 대신 이 번호가 실립니다.
번호는 한 메시지 안에서 겹치면 안 됩니다. 1부터 15까지는 번호가 바이트 한 개에 적히고, 16부터는 두 바이트 이상이 듭니다. 그래서 자주 채우는 필드에 작은 번호를 먼저 줍니다.
필드 타입으로는 미리 정해진 기본 타입을 씁니다. 자주 쓰는 것은 아래 다섯 줄입니다.
| proto 타입 | 담는 값 | 자바 코드에서 |
|---|---|---|
string |
글자 | String |
int32 · int64 |
정수 | int · long |
bool |
참거짓 | boolean |
double |
실수 | double |
bytes |
바이트열 | ByteString |
기본 타입 말고 다른 메시지 이름도 타입으로 쓸 수 있습니다. joined_at 필드가 그렇습니다. 메시지 안에 Timestamp 메시지가 하나 들어갑니다.
repeated 를 앞에 붙이면 같은 타입의 값을 여럿 담는 목록이 됩니다. tags 필드는 글자 여러 개를 담습니다. 자바 코드에서는 List 로 나옵니다.
필드 이름은 밑줄로 잇는 소문자로 적습니다. 만들어진 코드에서는 언어 관례에 맞춘 이름으로 바뀝니다. 자바라면 joined_at 을 읽는 메서드가 getJoinedAt() 이 됩니다.
proto2 에서는 필드를 반드시 채워야 하는 필수 필드로 정할 수 있었고, 비었을 때 쓸 기본값도 직접 정할 수 있었습니다. proto3 는 이 둘을 뺐습니다. 비워 보낸 필드는 받는 쪽에서 타입마다 정해진 값으로 읽힙니다. 숫자는 0, 글자는 빈 글자입니다.
열거형
열거형은 정해진 값 목록 가운데 하나만 담는 타입입니다. 회원 상태처럼 고를 수 있는 값이 미리 정해진 필드에 씁니다. 위 User 메시지의 status 필드가 이 타입을 씁니다.
enum Status {
STATUS_UNSPECIFIED = 0;
ACTIVE = 1;
BLOCKED = 2;
}
열거형 안의 = 1 은 필드 번호가 아닙니다. 그 값에 붙인 숫자입니다. 바이트에는 ACTIVE 라는 글자 대신 숫자 1 이 실립니다.
proto3 에서는 첫 값의 숫자가 반드시 0 이어야 합니다. 보내지 않은 필드는 받는 쪽에서 0 으로 읽히기 때문입니다. 그래서 첫 값에는 「아직 안 정해짐」을 뜻하는 이름을 붙이는 것이 관례입니다.
서비스와 rpc 메서드
proto 파일에는 데이터 말고 함수도 적을 수 있습니다. 다른 컴퓨터에 있는 함수를 내 코드의 함수처럼 부르는 방식을 원격 프로시저 호출(RPC, Remote Procedure Call)이라고 부릅니다.
service 는 이렇게 부를 함수 묶음을 적습니다. 아래는 사용자 한 명을 찾아 주는 서비스입니다.
service UserService {
rpc GetUser (GetUserRequest) returns (User);
}
message GetUserRequest {
string name = 1;
}
rpc 로 시작하는 줄 하나가 rpc 메서드 하나입니다. 괄호 안의 메시지를 받아 returns 뒤의 메시지를 돌려준다는 뜻입니다. 받는 것도 돌려주는 것도 메시지 하나씩입니다.
데이터와 함수를 언어와 상관없이 적는 언어를 인터페이스 정의 언어(IDL, Interface Definition Language)라고 부릅니다. proto 파일에 쓰는 문법이 이런 언어입니다.
gRPC(gRPC Remote Procedure Calls)는 이 서비스 정의를 읽어 원격 호출을 해 주는 프레임워크입니다.
코드를 만드는 컴파일러
proto 파일은 모양만 적은 문서라 그 자체로는 실행되지 않습니다. 이 파일을 읽어 언어별 코드를 만드는 프로그램이 protoc 입니다. 프로토콜 버퍼의 컴파일러입니다.
protoc -I proto --java_out=gen proto/user.proto
protoc -I proto --python_out=gen proto/user.proto
-I 는 입력 파일과 import 한 파일을 찾을 기준 폴더입니다. --java_out 과 --python_out 은 어느 언어의 코드를 어느 폴더에 만들지 정합니다. 첫 줄은 자바 클래스를 만듭니다. 둘째 줄은 user_pb2.py 라는 파이썬 모듈을 만듭니다.
파일을 읽어 코드를 뽑아내는 이 일을 코드 생성이라고 부릅니다. 한 파일에서 여러 언어의 코드가 나옵니다. 주문 서비스는 자바로, 추천 서비스는 파이썬으로 짜여 있어도 두 코드는 필드의 타입과 번호가 어긋나지 않습니다.
만들어진 코드는 손으로 고치지 않습니다. proto 파일을 고친 뒤 다시 만들면 손댄 부분이 덮여 사라집니다. 그래서 빌드할 때마다 protoc 이 돌도록 빌드 도구에 붙여 두는 경우가 많습니다.
여러 서비스가 한 파일을 나눠 갖는 방법
같은 파일을 여러 서비스가 가져야 하니 파일을 어디에 두느냐가 문제가 됩니다. 흔한 방법은 셋입니다.
| 방법 | 어떻게 | 대가 |
|---|---|---|
| 서비스마다 복사 | 파일을 서비스마다 Git 저장소에 복사해 둔다 | 한쪽만 고치면 사본끼리 어긋난다 |
| proto 전용 저장소 | proto 파일만 모은 저장소를 두고 서비스들이 가져다 쓴다 | 파일이 바뀌면 가져다 쓰는 쪽도 새 판으로 올려야 한다 |
| 만든 코드를 라이브러리로 배포 | proto 파일에서 만든 코드를 라이브러리로 묶어 내준다 | 언어마다 라이브러리를 따로 만들어야 한다 |
복사가 가장 손쉽지만 원본이 여럿이 됩니다. 원본이 여럿이면 계약도 여럿으로 갈립니다. 그래서 서비스가 늘면 원본을 한곳에 모으는 방법으로 옮겨 갑니다.
파일을 고칠 때 지키는 규칙
proto 파일은 한 번 쓰고 끝나지 않습니다. 서비스가 자라면 필드를 더하고 뺍니다. 이때 옛 파일로 만든 프로그램과 새 파일로 만든 프로그램이 한동안 같이 돕니다.
두 프로그램이 계속 주고받으려면 고치는 방법에 규칙이 있습니다. 스키마를 바꾸면서 옛 프로그램과 주고받을 수 있게 지키는 일을 스키마 진화라고 부릅니다.
| 바꾸는 것 | 옛 프로그램과 주고받나 | 까닭 |
|---|---|---|
| 새 번호로 필드를 더한다 | ✓ | 옛 프로그램은 모르는 번호를 건너뛴다 |
| 필드 이름을 바꾼다 | ✓ | 바이트에는 이름이 없다 |
| 필드 번호를 바꾼다 | ✗ | 옛 데이터의 값이 다른 필드로 들어간다 |
| 지운 필드의 번호를 새 필드에 준다 | ✗ | 옛 데이터의 값이 새 필드로 읽힌다 |
넷째 줄을 막으려고 지운 필드의 번호는 reserved 로 묶어 둡니다. 묶인 번호를 누가 다시 쓰면 protoc 이 코드를 만들지 않고 멈춥니다.
message User {
reserved 2; // 지운 age 의 번호
string name = 1;
}
쓰는 때와 안 쓰는 때
proto 파일은 프로토콜 버퍼를 쓰기로 하면 따라오는 파일입니다. 그래서 이 파일을 쓸지는 프로토콜 버퍼를 쓸지와 같은 물음입니다. 기준은 받는 쪽이 이 파일을 가질 수 있느냐입니다.
서비스끼리 안쪽에서 주고받는 호출이면 양쪽이 같은 파일을 가질 수 있습니다. 브라우저나 바깥 회사가 받는 공개 응답이면 그렇게 기대하기 어렵습니다. 그런 응답은 대개 JSON으로 보냅니다.
다른 포맷에도 proto 파일과 같은 일을 하는 파일이 있습니다. 모양을 적어 양쪽이 나눠 갖는다는 점은 같습니다. 적는 문법만 다릅니다.
| 포맷 · 방식 | 모양을 적는 파일 |
|---|---|
| 프로토콜 버퍼 | proto 파일 (.proto) |
| Apache Avro | JSON 으로 적는 스키마 파일 (.avsc) |
| Apache Thrift | Thrift 정의 파일 (.thrift) |
| 웹 요청으로 JSON 을 주고받는 서비스 | OpenAPI 문서 |
이 파일이 있어야만 바이트를 읽을 수 있다는 점은 장애를 볼 때 짐이 됩니다. 로그에 찍힌 바이트를 열어도 번호와 값만 보입니다. 읽으려면 그 데이터를 만든 판의 proto 파일을 찾아 protoc --decode 같은 도구에 함께 넣어야 합니다.
관련 항목
proto 파일 안에 적는 구성 요소
메시지 · 필드 · 필드 번호 · 열거형 · oneof · repeated 필드 · 맵 필드 · 패키지 · 잘 알려진 타입
proto 파일에서 코드를 만드는 도구
protoc · 컴파일러 · 코드 생성 · 컴파일러 플러그인 · Buf · 빌드 도구
proto 파일이 모양을 정해 주는 직렬화 포맷
프로토콜 버퍼 · 직렬화 · 역직렬화 · 와이어 타입 · 가변 길이 정수
proto 파일의 서비스 정의를 읽는 원격 호출 기술
gRPC · 원격 프로시저 호출 · 스텁 · 서비스 정의
proto 파일 대신 모양을 적는 다른 스키마 언어
Apache Avro · Apache Thrift · OpenAPI · JSON 스키마
proto 파일을 고칠 때 지키는 호환 규칙
스키마 진화 · 하위 호환 · 상위 호환 · 기본값 · 필드 존재 여부
proto 파일이 속하는 상위 분류
다른 이름: .proto 파일 · proto file · .proto file · 프로토 파일