Open Hae-Riri opened 3 years ago
프로토콜 버퍼를 사용하는 이유 중에 가장 많이 들리는 이유로 '더 작고 빨라서'이다. 이게 왜 더 작고 빠른지 알아보자.
구조화된 데이터를 직렬화하기 위한 유연하고 효율적인 자동화 매커니즘으로, xml보다 작고 빠르고 간단한 기법이다. 직렬화할 정보를 구조화하기 위해 개발자가 .proto 파일에 Protocol Buffer message 타입을 정의한다. 각 message는 작은 논리적 정보의 단위로, [이름-값]의 쌍을 포함하고 있다.
데이터를 파일에 쓰든 네트워크에 쓰든 컴퓨터는 결국 데이터를 0과 1로 나타내는 비트로 표현해야 한다. 만약 "hello"라는 문자 데이터를 utf-8로 인코딩해서 다른 서버에 보낸다면 h,e,l,l,o에 대한 인코딩을 통해 68(h), 65(e), 6C(l), 6C(l), 6F(o)와 같이 16진수로 표현될 것이다. 직렬화는 객체를 바이트 스트림으로 인코딩하는 것이다. 어떤 객체를 json 포맷으로 직렬화 했을 때 {"name" : "haerim"} 로 표현되듯, protocol buffer도 직렬화가 되는 하나의 포맷이다.
서로 다른 프로토콜 버퍼 포맷에서 어떻게 messsage가 인코딩되는지에 대해 알아보자.
Person이라는 클래스가 있다고 가정하고 json, protocol buffer를 생각해보자.
//json
{
"userName" : "Martin",
"favoriteNumber" : 1337,
"interests" : ["daydreaming", "hacking"]
}
이렇게 되면 공백을 제외하고 총 82바이트가 사용된다.
//proto
message Person{
required string userName = 1;
optional int32 favoriteNumber = 2;
repeated string interests = 3;
}
위와 같은 프로토콜 버퍼 스키마를 사용해서 데이터를 인코딩하면 33바이트로 표현이 가능하다.
커다란 네모 속에서 작은 네모로 이루어진 부분을 세어 보면 33바이트가 된다.
구성요소
첫 번째 필드(String) 인 userName에 대해서만 먼저 보자면 아래와 같이 구성된다.
Martin
에 대해 16진수인 06이 들어갔다.두 번째 필드(varint) 인 favoriteNumber, 1337에 대해 보면 아래와 같다.
세 번째 필드(다중값) 인 interests 에 대해 보면 아래와 같다.
먼저 varint를 알아야 하는데, varints는 하나 이상의 바이트를 사용해서 정수를 직렬화하는 방법이다. varints 안의 각 바이트는 마지막 바이트만 제외하고 모두 최상위비트가 설정되는데, 이건 뒤따라 오는 비트가 있는지 없는지를 나타내는 용도이다.
예를 들어 숫자 1을 표현하면, 이건 1바이트니까 최상위비트가 설정될 필요는 없다. 0000 0001
그런데 300일 경우에는 아래와 같이 된다.
1010 1100 0000 0010
이걸 300이라고 알아차리려면,
1010 1100 0000 0010
→ 010 1100 000 0010
000 0010 010 1100
→ 000 0010 ++ 010 1100
→ 100101100
→ 256 + 32 + 8 + 4 = 300
protocol buffer message 는 key-value 쌍이 쭉 이어져 있다. 직렬화해서 바이너리로 만들면 key는 각 필드에 할당한 태그번호가 된다. 바이너리 안에는 필드 이름이 없기 때문에, 정확한 필드의 이름과 타입은 디코딩 되어야만 알 수 있다.
그러다 만약 디코딩 중에 모르는 key가 나온다면 그냥 패스해버린다. 이런 이유로 프로그램을 새로 짜지 않고도 새로운 필드를 추가할 수 있게 된다.
위에서 각 필드가 변환된 바이너리를 살펴보면서 String, int32냐에 따라 바이너리가 붙었었는데 그것도 여러 종류가 있다. 아까 보면 맨 앞 8비트 중에 5비트를 필드 번호 표현에 사용하고 나머지 3비트를 wire type 표현에 사용한다.
예를 들어 08 96 01
이라고 직렬화되었다면 이게 의미하는 것은,
같은 데이터를 보내더라도 데이터의 크기가 작으니까 같은 시간에 더 많은 데이터를 보낼 수 있다. 위에서 인코딩한 것만 봐도, 확실히 크기가 작아짐을 확인할 수 있다!
일반적으로는 json 포맷으로 온 데이터를 다시 객체로 파싱해서 객체로 사용하는데, 프로토콜 버퍼를 쓰면 바이트가 왔을 때 그대로 메모리에 써 버리고 객체 레퍼런스가 가리켜 버리면 된다. 별도로 파싱할 필요가 없다.
대신 단점은,
json은 데이터를 봤을 때 사람이 읽기 편한데, 프로토콜 버퍼는 proto 파일 없이는 의미를 알 수가 없다. 즉 proto 파일이 꼭 필요하고, 이게 xml 스키마를 대체하는 용도다.
프로토콜버퍼, 코루틴
rpc 원격 프로시저 호출 프로그램을 실행할 수 있도록 하는 프로토콜로, 다른 컴퓨터에 있는 다른 프로그램의 절차를 실행시킬 수 있음. 개발자가 네트워크 상호 작용의 세부 사항을 명시적으로 코딩할 필요가 없음.
클라이언트는 서버와 동일한 방식이나 기능을 제공하는 stub을 갖고 있음. 이 stub은 gRPC에 의해 자동으로 생성된다.
stub은 네트워크를 통해 서버와 정보를 주고받기 위해 후드 아래에서 gRPC 프레임워크를 호출한다.
gRPC가 어떻게 protocol buffer와 함께 코드를 생성해낼까? 우리가 welcome.proto를 작성하면, protocol bueffer compiler에 의해 server - client stub이 생성됨. 각 프로그래밍 언어별 grpc plugin에 의해 다양하게 생성됨.
왜 gRPC가 protocol buffer 를 사용할까?
무엇이 grpc를 효과적으로 만드는가? http/2를 전송 프로토콜로 사용하기 때문에 http/2의 장점을 가진다.