언어/C#

Protobuf

에린_1 2024. 8. 14. 09:43
728x90

Protobuf

  • gRPC는 IDL(Interface Design Language)로 Protobuf를 사용한다. Protobuf IDL은 gRPC 서비스에서 보내고 받는 메시지를 지정하기 위한 언어 중립적인 형식이다. Protobuf 메시지는 파일에 정의 .proto 된다.

Protobuf 메시지

  • 메시지는 Protobuf의 기본 데이터 전송 개체이다. 개념상 .NET 클래스와 유사하다.
syntax = "proto3";

option csharp_namespace = "Contoso.Messages";

message Person {
    int32 id = 1;
    string first_name = 2;
    string last_name = 3;
}
  • 위의 메시지 정의는 세 필드를 이름-값 쌍으로 지정한다. .NET 형식의 속성과 마찬가지로 각 필드에는 이름과 형식이 있다. 필드 형식은 Protobuf 스칼라 값 형식일 수 있다.
  • Protobuf 스타일 가이드에서는 필드 이름에 underscore_separated_names를 사용하도록 권장한다. .NET 앱용으로 생성된 새 Protobuf 메시지는 Protobuf 스타일 지침을 따라야 한다. .NET 도구는 .NET 명명 표준을 사용하는 .NET 형식을 자동으로 생성한다. 예를 들어 first_name Protobuf 필드는 FirstName .NET 속성을 생성한다.
  • 메시지 정의의 각 필드에는 이름 외에도 고유한 번호가 있다. 필드 번호는 메시지가 Protobuf로 직렬화될 때 필드를 식별하는 데 사용된다. 작은 수를 직렬화하는 것은 전체 필드 이름을 직렬화하는 것보다 더 빠르다. 필드 번호는 필드를 식별하기 때문에 필드를 변경할 때는 주의해야 한다.

스칼라 값 형식

Protobuf 형식 C# 형식

double double
float float
int32 int
int64 long
uint32 uint
uint64 ulong
sint32 int
sint64 long
fixed32 uint
fixed64 ulong
sfixed32 int
sfixed64 long
bool bool
string string
bytes ByteString
  • 스칼라 값에는 항상 기본값이 있으며 null 로 설정할 수 없다. 이 제약 조건에는 C# 클래스인 string 과 ByteString 이 포함된다. string 기본값은 빈 문자열 값이며 ByteString 기본값은 빈 바이트 값이다. 기본값을 null 로 설정하려고 하면 오류가 throw 된다.
  • null 허용 래퍼 형식을 사용하여 null 값을 지원할 수 있다.

날짜 및 시간

  • 네이티브 스칼라 형식은 .NET의 DateTimeOffset, DateTime, TimeSpan에 해당하는 날짜 및 시간 값을 제공하지 않는다. 해당 형식은 Protobuf의 잘 알려진 형식 확장 중 일부를 사용하여 지정할 수 있다. 이 확장은 지원되는 플랫폼에서 복합 필드 형식을 위한 코드 생성과 런타임을 지원한다.

.NET 형식 Protobuf 잘 알려진 형식

DateTimeOffset google.protobuf.Timestamp
DateTime google.protobuf.Timestamp
TimeSpan google.protobuf.Duration
syntax = "proto3";

import "google/protobuf/duration.proto";  
import "google/protobuf/timestamp.proto";

message Meeting {
    string subject = 1;
    google.protobuf.Timestamp start = 2;
    google.protobuf.Duration duration = 3;
}
  • C# 클래스에서 생성되는 속성은 .NET 날짜 및 시간 형식이 아니다. 해당 속성은 Google.Protobuf.WellKnownTypes 네임스페이스의 Timestamp 클래스와 Duration 클래스를 사용하며, 이 클래스는 DateTimeOffset, DateTime, TimeSpan 으로 /에서 변환하는 메서드를 제공한다.
// Create Timestamp and Duration from .NET DateTimeOffset and TimeSpan.
var meeting = new Meeting
{
    Time = Timestamp.FromDateTimeOffset(meetingTime), // also FromDateTime()
    Duration = Duration.FromTimeSpan(meetingLength)
};

// Convert Timestamp and Duration to .NET DateTimeOffset and TimeSpan.
var time = meeting.Time.ToDateTimeOffset();
var duration = meeting.Duration?.ToTimeSpan();
  • Timestamp 형식은 UTC 시간으로 작동한다. DateTimeOffset 값의 오프셋은 항상 0이며 DateTime.Kind 속성은 항상 DateTimeKind.Utc 이다.

Nullable 유형

  • C#에 해당하는 Protobuf 코드를 생성하는 데에는 네이티브 형식을 사용한다. 따라서 값이 항상 포함되며 null 일 수 없다.
  • C# 코드에 int? 가 사용되는 것과 같이 명시적인 null 이 필요한 값의 경우 Protobuf의 잘 알려진 형식은 null 허용 C# 형식으로 컴파일되는 래퍼를 포함한다. 이 래퍼를 사용하려면 다음 코드와 같이 .proto 파일에 wrappers.proto 를 가져온다.
syntax = "proto3";

import "google/protobuf/wrappers.proto";

message Person {
    // ...
    google.protobuf.Int32Value age = 5;
}
  • wrappers.proto 형식은 생성된 속성에 노출되지 않는다. Protobuf는 C# 메시지에서 적절한 .NET null 허용 형식에 자동으로 매핑한다. 예를 들어 google.protobuf.Int32Value 필드는 int? 속성을 생성한다. string 및 ByteString 같은 참조 형식 속성은 오류 없이 null 을 할당할 수 있는 경우를 제외하고는 변경되지 않는다.

C# 형식 잘 알려진 형식 래퍼

bool? google.protobuf.BoolValue
double? google.protobuf.DoubleValue
float? google.protobuf.FloatValue
int? google.protobuf.Int32Value
long? google.protobuf.Int64Value
uint? google.protobuf.UInt32Value
ulong? google.protobuf.UInt64Value
string google.protobuf.StringValue
ByteString google.protobuf.BytesValue

바이트

  • 이진 페이로드는 Protobuf에서 bytes 스칼라 값 형식으로 지원된다. C#에서 생성된 속성은 ByteString 을 속성 형식으로 사용한다.
  • ByteString.CopyFrom(byte[] date) 을 사용하여 바이트 배열에서 새 인스턴스를 만든다.
var data = await File.ReadAllBytesAsync(path);

var payload = new PayloadResponse();
payload.Data = ByteString.CopyFrom(data);
  • ByteString 데이터는 ByteString.Span 또는 ByteString.Memory 를 사용하여 직접 액세스한다. 또는 ByteString.ToByteArray() 를 호출하여 인스턴스를 바이트 배열로 다시 변환한다.
var payload = await client.GetPayload(new PayloadRequest());

await File.WriteAllBytesAsync(path, payload.Data.ToByteArray());

10진수

  • Protobuf는 기본적으로 .NET decimal 형식을 지원하지 않으며 double 및 float 만 지원한다. Protobuf 프로젝트에서는 표준 10진수 형식을 잘 알려진 형식에 추가하고 언어에 대한 플랫폼 지원 및 이 플랫폼을 지원하는 프레임워크를 포함하는 가능성에 대해 지속해서 논의하고 있다. 아직 아무것도 구현되지 않았다.
  • .NET 클라이언트와 서버 간에 안전하게 직렬화하는 데 사용할 수 있는 decimal 형식을 나타내는 메시지 정의를 만들 수 있다. 그러나 다른 플랫폼의 개발자는 사용되는 형식을 이해하고 이 형식에 대한 고유한 처리를 구현해야 한다.
    • decimal 값을 바이트 문자열로 인코딩하는 데 추가 알고리즘을 사용할 수 있다. 다음은 DecimalValue 에서 사용하는 알고리즘이다.
      • 이해하기 쉽다.
      • 다양한 플랫폼에서 big-endian 또는 little-endian의 영향을 받지 않는다.
      • decimal 의 전체 범위가 아닌 소수 자릿수 9자리의 최대 전체 자릿수를 사용하여 양수 9,223,372,036,854,775,807.999999999에서 음수 9,223,372,036,854,775,808.999999999까지의 10진수를 지원한다.

컬렉션

  • 목록
    • Protobuf의 목록은 필드에 repeated 접두사 키워드를 사용하여 지정한다.
    • 생성된 코드에서 repeated 필드는 Google.Protobuf.Collections.RepeatedField<T> 제네릭 형식으로 표시된다.
    public class Person
    {
        // ...
        public RepeatedField<string> Roles { get; }
    }
    
    • RepeatedField<T> 는 IList<T>를 구현한다. 따라서 LINQ 쿼리를 사용하거나 배열 또는 목록으로 변환할 수 있다. RepeatedField<T> 속성에는 public setter가 없다. 항목을 기존 컬렉션에 추가해야 한다.
    var person = new Person();
    
    // Add one item.
    person.Roles.Add("user");
    
    // Add all items from another collection.
    var roles = new [] { "admin", "manager" };
    person.Roles.Add(roles);
    
  • 사전
    • .NET IDictionary<TKey, TValue> 형식은 Protobuf에서 map<key_type, value,type>을 사용하여 표시된다.
    message Person {
        // ...
        map<string, string> attributes = 9;
    }
    
    • 생성된 .NET 코드에서 map 필드는 Google.Protobuf.Collections.MapField<TKey, TValue> 제네릭 형식으로 표시된다. MapField<TKey, TValue> 는 IDictionary<TKey, TValue>를 구현한다. repeated 속성과 마찬가지로 map 속성에도 public setter가 없다. 항목을 기존 컬렉션에 추가해야 한다.
    var person = new Person();
    
    // Add one item.
    person.Attributes["created_by"] = "James";
    
    // Add all items from another collection.
    var attributes = new Dictionary<string, string>
    {
        ["last_modified"] = DateTime.UtcNow.ToString()
    };
    person.Attributes.Add(attributes);
    

비구조적 메시지 및 조건부 메시지

  • Protobuf는 계약 중심 메시징 형식이다. 필드와 유형을 포함하여 앱의 메시지는 앱을 빌드할 때 .proto 파일에서 지정해야 한다. Protobuf의 계약 중심 디자인은 메시지 콘텐츠를 적용하는 데 적합하지만 다음과 같이 엄격한 계약이 필요하지 않은 시나리오를 제한할 수 있다.
    • 알 수 없는 페이로드가 포함된 메시지. 예를 들어 메시지를 포함할 수 있는 필드가 있는 메시지이다.
    • 조건부 메시지. 예를 들어 gRPC 서비스에서 반환되는 메시지는 성공 결과 또는 오류 결과일 수 있다.
    • 동적 값. 예를 들어 JSON과 유사한 비구조적 값 컬렉션을 포함하는 필드가 있는 메시지이다.
  • Protobuf는 해당 시나리오를 지원하기 위한 언어 기능과 형식을 제공한다.

모두

  • 이 형식을 Any 사용하면 멤시지를 정의 없이 포함된 형식으로 사용할 수 있다. .proto .Any 형식을 사용하려면 any.proto 를 가져온다.
import "google/protobuf/any.proto";

message Status {
    string message = 1;
    google.protobuf.Any detail = 2;
}
// Create a status with a Person message set to detail.
var status = new ErrorStatus();
status.Detail = Any.Pack(new Person { FirstName = "James" });

// Read Person message from detail.
if (status.Detail.Is(Person.Descriptor))
{
    var person = status.Detail.Unpack<Person>();
    // ...
}

Oneof

  • oneof 필드는 언어 기능이다. 컴파일러는 메시지 클래스를 생성할 때 oneof 키워드를 처리한다. oneof 를 사용하여 Person 또는 Error 를 반환할 수 있는 응답 메시지를 지정하는 것은 다음과 같다.
message Person {
    // ...
}

message Error {
    // ...
}

message ResponseMessage {
  oneof result {
    Error error = 1;
    Person person = 2;
  }
}
  • oneof 집합 내의 필드는 전체 메시지 선언에서 고유한 필드 번호가 있어야 한다.
  • oneof 를 사용하면 생성된 C# 코드에는 설정된 필드를 지정하는 열거형이 포함된다. 열거형을 테스트하여 설정된 필드를 찾을 수 있다. 설정되지 않은 필드는 예외를 throw하는 대신 null 또는 기본값을 반환한다.
var response = await client.GetPersonAsync(new RequestMessage());

switch (response.ResultCase)
{
    case ResponseMessage.ResultOneofCase.Person:
        HandlePerson(response.Person);
        break;
    case ResponseMessage.ResultOneofCase.Error:
        HandleError(response.Error);
        break;
    default:
        throw new ArgumentException("Unexpected result.");
}

  • Value 형식은 동적으로 형식화된 값을 나타낸다. null , 숫자, 문자열, 부울, 값의 사전(Struct ) 이거나 값의 목록(ValueList)일 수 있다. Value 는 앞에서 설명한 oneof 기능을 사용하는 Protobuf 잘 알려진 형식이다. Value 형식을 사용하려면 struct.proto 를 추가해야 한다.
import "google/protobuf/struct.proto";

message Status {
    // ...
    google.protobuf.Value data = 3;
}
// Create dynamic values.
var status = new Status();
status.Data = Value.ForStruct(new Struct
{
    Fields =
    {
        ["enabled"] = Value.ForBool(true),
        ["metadata"] = Value.ForList(
            Value.ForString("value1"),
            Value.ForString("value2"))
    }
});

// Read dynamic values.
switch (status.Data.KindCase)
{
    case Value.KindOneofCase.StructValue:
        foreach (var field in status.Data.StructValue.Fields)
        {
            // Read struct fields...
        }
        break;
    // ...
}
  • Value 를 직접 사용하면 길어질 수 있다. Value 의 대안으로 Protobuf에서는 메시지를 JSON에 매핑하도록 지원을 기본 제공한다. Protobuf의 JsonFormatter 형식과 JsonWriter 형식을 Protobuf 메시지에 사용할 수 있다. Value 는 JSON으로 변환하는 데 특히 적합하다.
  • 다음은 위의 코드와 동일한 JSON 코드이다.
// Create dynamic values from JSON.
var status = new Status();
status.Data = Value.Parser.ParseJson(@"{
    ""enabled"": true,
    ""metadata"": [ ""value1"", ""value2"" ]
}");

// Convert dynamic values to JSON.
// JSON can be read with a library like System.Text.Json or Newtonsoft.Json
var json = JsonFormatter.Default.Format(status.Data);
var document = JsonDocument.Parse(json);

repeated

  • 프로토콜 버퍼(Protobuf)에서 repeated 키워드는 메시지 내의 특정 필드가 여러 번 반복될 수 있음을 나타낸다. 이를 통해 동일한 타입의 여러 값을 포함하는 배열이나 리스트와 같은 데이터 구조를 표현할 수 있다.

주요 특징

  1. 중복 허용
    • repeated 필드는 동일한 값이 여러 번 포함될 수 있다. 즉, 리스트 내에 중복된 항목이 있을 수 있다.
  2. 순서 유지
    • 추가된 요소의 순서가 유지된다. 이는 추가한 순서대로 요소를 순회할 수 있음을 의미한다.
  3. 기본값
    • repeated 필드는 메시지가 생성될 때 기본적으로 빈 리스트로 초기화가 된다.

활용 예시

  • repeated 필드는 여러 가지 상황에서 유용하게 사용할 수 있다.
    • 이벤트 목록, 이메일 주소 목록 등과 같이 여러 값을 저장해야 할 때
    • 이벤트 로그, 거래 내역 등과 같이 시퀀스가 중요한 데이터를 저장할 때
    • 여러 개의 하위 메시지를 포함해야 할 때

주의사항

  • repeated 필드는 일반적으로 데이터 크기를 증가시킬 수 있으므로, 필요한 경우에만 사용하는 것이 좋다..
  • 비어 있을 때와 값이 설정되지 않았을 때의 차이를 명확히 구분해야 한다. 비어 있는 repeated 필드는 빈 리스트로 존재한다.
728x90

'언어 > C#' 카테고리의 다른 글

CompareTo  (0) 2024.08.16
Exception has been thrown by the target of an invocation  (0) 2024.08.16
MergeFrom  (0) 2024.08.12
JsonFormatter.ToDiagnosticString  (0) 2024.08.12
ProtoMsg.ProtocolMessageReflection.Descriptor.MessageTypes  (0) 2024.08.12