이 게시글을 통해 다음을 이해할 수 있습니다.
먼저 아주 간단한 예를 살펴보겠습니다. 각 검색 요청에 쿼리 문자열, 관심 있는 특정 결과 페이지, 페이지당 결과 수가 포함된 검색 요청 메시지 형식을 정의하고 싶다고 가정해 보겠습니다. 다음은 메시지 형식을 정의하는 데 사용하는 .proto 파일입니다.
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
}
앞의 예에서 모든 필드는 스칼라 유형으로, 두 개의 정수(page_number 및 results_per_page)와 하나의 문자열(쿼리)입니다. 필드에 다른 메시지 유형처럼 열거형 및 복합 유형을 지정할 수도 있습니다.
메시지 정의의 각 필드에 1에서 536,870,911 사이의 숫자를 지정해야 하며, 다음과 같은 제한이 있습니다:
이 번호는 메시지 와이어 형식의 필드를 식별하기 때문에 메시지 유형이 사용 중이면 변경할 수 없습니다. “필드 번호를 '변경'한다는 것은 해당 필드를 삭제하고 유형은 같지만 번호가 바뀐 새 필드를 만드는 것과 같습니다. 이 작업을 올바르게 수행하는 방법은 필드 삭제하기를 참조하세요.
필드 번호는 절대로 재사용해서는 안 됩니다. 새 필드 정의에 재사용하기 위해 예약된 목록에서 필드 번호를 가져와서는 안 됩니다. 필드 번호 재사용의 결과를 참조하세요.
가장 자주 설정되는 필드에는 필드 번호 1부터 15까지를 사용해야 합니다. 필드 번호 값이 낮을수록 와이어 형식에서 더 적은 공간을 차지합니다. 예를 들어 1~15 범위의 필드 번호는 인코딩하는 데 1바이트가 소요됩니다. 16~2047 범위의 필드 번호는 2바이트가 소요됩니다. 이에 대한 자세한 내용은 프로토콜 버퍼 인코딩에서 확인할 수 있습니다.
필드 번호를 재사용하면 와이어 형식 메시지 디코딩이 모호해집니다.
프로토뷰 와이어 형식은 간결하며 한 정의를 사용하여 인코딩된 필드와 다른 정의를 사용하여 디코딩된 필드를 감지할 수 있는 방법을 제공하지 않습니다.
한 정의를 사용하여 필드를 인코딩한 다음 다른 정의로 동일한 필드를 디코딩하면 문제가 발생할 수 있습니다
필드 번호 재사용의 일반적인 원인
필드의 와이어 형식을 지정하는 데 3비트가 사용되므로 필드 번호는 32비트가 아닌 29비트로 제한됩니다. 이에 대한 자세한 내용은 인코딩 항목을 참조하세요.
메시지 필드는 다음 중 하나를 사용할 수 있습니다
protobuf editions 및 proto2 와의 호환성을 극대화하려면 implicit 필드보다 optional 필드를 사용하는 것이 좋습니다.
proto3에서 스칼라 숫자 유형의 반복 필드는 기본적으로 패킹 인코딩을 사용합니다.
패킹 인코딩에 대한 자세한 내용은 프로토콜 버퍼 인코딩에서 확인할 수 있습니다.
proto3에서는 메시지 유형 필드에 이미 필드 존재 여부가 있습니다. 따라서 선택적 수정자를 추가해도 필드에 대한 필드 존재 여부는 변경되지 않습니다.
다음 코드 샘플의 Message2 및 Message3에 대한 정의는 모든 언어에 대해 동일한 코드를 생성하며 바이너리, JSON 및 TextFormat으로 표현하는 데 차이가 없습니다
syntax="proto3";
package foo.bar;
message Message1 {}
message Message2 {
Message1 foo = 1;
}
message Message3 {
optional Message1 bar = 1;
}
“well-formed"이라는 용어는 protobuf 메시지에 적용될 때 직렬화/역직렬화된 바이트를 의미합니다. 프로토 구문 분석기는 주어진 프로토 정의 파일이 구문 분석이 가능한지 확인합니다.
단수 필드는 와이어 형식 바이트에 두 번 이상 나타날 수 있습니다. 구문 분석기는 입력을 받아들이지만 생성된 바인딩을 통해 해당 필드의 마지막 인스턴스만 액세스할 수 있습니다. 이 주제에 대한 자세한 내용은 마지막 인스턴스가 승리하기를 참조하세요.
하나의 .proto 파일에 여러 메시지 유형을 정의할 수 있습니다. 이 기능은 여러 개의 관련 메시지를 정의하는 경우에 유용합니다. 예를 들어 SearchResponse 메시지 유형에 해당하는 답장 메시지 형식을 정의하려는 경우 동일한 .proto 파일에 추가할 수 있습니다:
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 results_per_page = 3;
}
message SearchResponse {
...
}
메시지 결합으로 인한 부피 증가 메시지, 열거형, 서비스 등 여러 메시지 유형을 하나의 .proto 파일에 정의할 수 있지만 다양한 종속성을 가진 많은 수의 메시지를 하나의 파일에 정의하면 종속성 부피가 커질 수도 있습니다. .proto 파일당 가능한 한 적은 수의 메시지 유형을 포함하는 것이 좋습니다.
.proto 파일에 주석을 추가하려면 다음과 같이 하세요:
필드 삭제는 제대로 수행하지 않으면 심각한 문제를 일으킬 수 있습니다.
필드가 더 이상 필요하지 않고 클라이언트 코드에서 모든 참조가 삭제된 경우 메시지에서 필드 정의를 삭제할 수 있습니다. 하지만 삭제된 필드 번호는 반드시 예약해야 합니다. 필드 번호를 예약하지 않으면 나중에 개발자가 해당 번호를 재사용할 수 있습니다.
또한 메시지의 JSON 및 TextFormat 인코딩이 계속 구문 분석할 수 있도록 필드 이름을 예약해야 합니다.
필드를 완전히 삭제하거나 주석 처리하여 메시지 유형을 업데이트하면 향후 개발자가 해당 유형을 업데이트할 때 필드 번호를 재사용할 수 있습니다. 이로 인해 필드 번호 재사용의 결과에서 설명한 대로 심각한 문제가 발생할 수 있습니다. 이런 일이 발생하지 않도록 하려면 삭제된 필드 번호를 예약 목록에 추가하세요.
향후 개발자가 이러한 예약된 필드 번호를 사용하려고 하면 프로토 컴파일러에서 오류 메시지를 생성합니다.
message Foo {
reserved 2, 15, 9 to 11;
}
예약된 필드 번호 범위는 포괄적입니다(9 to 11은 9, 10, 11과 동일)
필드 이름이 직렬화되는 TextProto 또는 JSON 인코딩을 사용하는 경우를 제외하고는 이전 필드 이름을 나중에 재사용하는 것이 일반적으로 안전합니다. 이러한 위험을 방지하려면 삭제된 필드 이름을 예약 목록에 추가하면 됩니다.
예약된 이름은 한 가지 예외를 제외하고는 프로토 컴파일러 동작에만 영향을 미치며 런타임 동작에는 영향을 미치지 않습니다: TextProto 구현은 구문 분석 시 예약된 이름으로 알 수 없는 필드를 (다른 알 수 없는 필드처럼 오류를 발생시키지 않고) 삭제할 수 있습니다(현재 C++ 및 Go 구현만 그렇게 함). 런타임 JSON 구문 분석은 예약된 이름의 영향을 받지 않습니다.
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
동일한 예약 문에서 필드 이름과 필드 번호를 혼합할 수 없다는 점에 유의하세요.
메시지를 구문 분석할 때 인코딩된 메시지 바이트에 특정 필드가 포함되어 있지 않은 경우 구문 분석된 개체에서 해당 필드에 액세스하면 해당 필드의 기본값이 반환됩니다. 기본값은 유형별로 다릅니다
반복 필드의 기본값은 비어 있습니다(일반적으로 해당 언어로 된 빈 목록).
지도 필드의 기본값은 비어 있습니다(일반적으로 적절한 언어로 된 빈 지도).
암시적 존재 스칼라 필드의 경우 메시지를 구문 분석하면 해당 필드가 명시적으로 기본값으로 설정되었는지(예: 부울이 거짓으로 설정되었는지) 아니면 아예 설정되지 않았는지 알 수 없으므로 메시지 유형을 정의할 때 이 점을 염두에 두어야 합니다. 예를 들어, 특정 동작이 기본적으로 발생하지 않도록 하려면 거짓으로 설정된 경우 특정 동작을 켜는 부울을 사용하지 마세요. 또한 스칼라 메시지 필드가 기본값으로 설정되어 있으면 해당 값이 와이어에서 직렬화되지 않는다는 점에 유의하세요. 실수 또는 이중 값이 +0으로 설정된 경우 직렬화되지 않지만 -0은 고유한 값으로 간주되어 직렬화됩니다.
메시지 유형을 정의할 때 해당 필드 중 하나에 미리 정의된 값 목록 중 하나만 포함되도록 하고 싶을 수 있습니다. 예를 들어 각 SearchRequest에 대해 말뭉치 필드를 추가하고 싶다고 가정해 보겠습니다. 여기서 말뭉치는 UNIVERSAL, 웹, 이미지, 로컬, 뉴스, 제품 또는 비디오가 될 수 있습니다. 메시지 정의에 가능한 각 값에 대한 상수를 사용하여 열거형을 추가하면 아주 간단하게 이 작업을 수행할 수 있습니다.
다음 예에서는 가능한 모든 값을 포함하는 Corpus라는 열거형과 Corpus 유형의 필드를 추가했습니다
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 results_per_page = 3;
Corpus corpus = 4;
}
SearchRequest.corpus 필드의 기본값은 열거형에 정의된 첫 번째 값이기 때문에 CORPUS_UNSPECIFIED입니다.
proto3에서 열거형 정의에 정의된 첫 번째 값은 값이 0이어야 하며 이름은 ENUM_TYPE_NAME_UNSPECIFIED 또는 ENUM_TYPE_NAME_UNKNOWN이어야 합니다. 그 이유는 다음과 같습니다
또한 이 첫 번째 기본값은 “이 값은 지정되지 않았습니다”라는 의미 이외의 다른 의미는 갖지 않는 것이 좋습니다.
다른 메시지 유형을 필드 유형으로 사용할 수 있습니다. 예를 들어 각 SearchResponse 메시지에 결과 메시지를 포함시키고 싶다고 가정해 보겠습니다. 이렇게 하려면 동일한 .proto에 결과 메시지 유형을 정의한 다음 SearchResponse에 결과 유형의 필드를 지정하면 됩니다
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
앞의 예에서 Result 메시지 유형이 SearchResponse와 같은 파일에 정의되어 있는데, 필드 유형으로 사용하려는 메시지 유형이 이미 다른 .proto 파일에 정의되어 있다면 어떻게 해야 할까요?
다른 .proto 파일에서 정의를 가져와서 사용할 수 있습니다. 다른 .proto 파일의 정의를 가져오려면 파일 맨 위에 가져오기 문을 추가하면 됩니다
import "myproject/other_protos.proto";
기본적으로 직접 가져온 .proto 파일에서만 정의를 사용할 수 있습니다. 그러나 때로는 .proto 파일을 새 위치로 이동해야 할 수도 있습니다. .proto 파일을 직접 이동하고 한 번의 변경으로 모든 호출 사이트를 업데이트하는 대신, 가져오기 공개 개념을 사용하여 모든 가져오기를 새 위치로 전달하도록 이전 위치에 플레이스홀더 .proto 파일을 넣을 수 있습니다.
공개 가져오기 기능은 Java, Kotlin, TypeScript, JavaScript, GCL 및 프로토비프 정적 리플렉션을 사용하는 C++ 대상에서는 사용할 수 없다는 점에 유의하세요.
import public 종속성은 import public 문을 포함하는 프로토타입을 임포트하는 모든 코드에서 일시적으로 의존할 수 있습니다. 예를 들어
// new.proto
// All definitions are moved here
// old.proto
// This is the proto that all clients are importing.
import public "new.proto";
import "other.proto";
// client.proto
import "old.proto";
// You use definitions from old.proto and new.proto, but not other.proto
프로토콜 컴파일러는 프로토콜 컴파일러 명령줄에 지정된 디렉터리 집합에서 -I/-프로토_경로 플래그를 사용하여 가져온 파일을 검색합니다. 플래그가 지정되지 않은 경우 컴파일러가 호출된 디렉터리를 찾습니다. 일반적으로 --proto_path 플래그를 프로젝트의 루트로 설정하고 모든 임포트에 정규화된 이름을 사용해야 합니다.
다음 예제에서와 같이 다른 메시지 유형 내에서 메시지 유형을 정의하고 사용할 수 있습니다. 여기서는 검색 응답 메시지 내에 결과 메시지가 정의되어 있습니다
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}