필드 번호를 바꿨더니 옛 클라이언트가 조용히 틀린 값을 읽었다 · 바이트 한 줄이 필드 번호를 품고 있다 · 실습
바이트를 손으로 읽고 쓴다
목표
protoc 도 protobuf 패키지도 없는 파드에서, 파이썬 표준 라이브러리만으로 protobuf
와이어 포맷의 인코더·디코더를 직접 만듭니다. varint → 태그 → 부호 있는 정수 →
필드 → 메시지 순으로 쌓아 올리고, 마지막에는 서브메시지와 packed repeated 를
해독합니다. 만든 도구는 다음 실습에서도 씁니다.
왜 중요한가
gRPC 장애의 절반은 "바이트에 이름이 없다" 는 사실을 몰라서 생깁니다. 와이어에는
필드 번호와 와이어 타입만 실리고, 이름과 선언 타입은 읽는 쪽 .proto 가
붙입니다. 이걸 손으로 한 번 인코딩해 보면 왜 번호를 바꾸면 안 되는지, 왜int32 음수가 10바이트나 되는지, 왜 옛 클라이언트가 새 필드를 오류 없이
건너뛰는지가 계산으로 보입니다. 라이브러리가 감춰 둔 것을 한 번 열어 본 사람은
스키마 리뷰에서 다른 것을 봅니다.
단계
1. /root/grpc/pb.py 를 만들고 encode_varint(n) -> bytes 와 decode_varint(data, pos=0) -> (값, 다음 위치) 를 구현하세요. 1, 150, 300, 16384 를 인코딩한 16진수를 /root/grpc/01-varint.txt 에 값=16진수 로 한 줄씩 적으세요.
2. encode_tag(field, wire_type) -> bytes 와 decode_tag(data, pos=0) -> (번호, 와이어타입, 다음 위치) 를 더하세요. 1:0 2:2 3:2 16:0 네 태그의 16진수를 /root/grpc/02-tag.txt 에 번호:와이어타입=16진수 로 적으세요.
3. zigzag_encode(n) -> int 와 zigzag_decode(u) -> int 를 더하고, int32 -1 sint32 -1 int32 -150 sint32 -150 을 실제로 인코딩해 /root/grpc/03-signed.txt 에 타입 값 = 16진수 (N bytes) 로 적으세요. int32 는 64비트 2의 보수를 varint 로, sint32 는 ZigZag 를 거친 값을 varint 로 보냅니다.
4. encode_field(field, kind, value) -> bytes 를 더하세요. kind 는 int32 int64 uint32 uint64 sint32 sint64 bool string bytes 입니다. 그 함수로 id(1)=4242, qty(2)=17, note(3)="한글 메모" 인 주문을 만드는 /root/grpc/04-make.py 를 쓰고 실행해 /root/grpc/04-order.bin 을 남기세요.
5. decode_message(data) -> [(번호, 와이어타입, 원시값), ...] 를 더하세요. VARINT 는 int, LEN 은 bytes, I32/I64 는 4·8바이트 bytes 그대로입니다. /opt/app/grpc/order_v1.bin 을 해독해 /root/grpc/05-decode.txt 에 field=N wire=W value=V 로 한 줄씩 적으세요(LEN 값은 16진수).
6. encode_records(records) -> bytes (decode_message 의 역함수) 를 더하세요. /opt/app/grpc/order_v2.bin 을 해독해 레코드마다 v1 스키마(/opt/app/grpc/order_v1.proto)가 아는 번호인지 /root/grpc/06-unknown.txt 에 field=N wire=W known 또는 unknown 으로 적으세요.
7. /opt/app/grpc/nested.bin 의 필드 6 은 Customer 서브메시지입니다. 그 페이로드를 다시 decode_message 로 해독해 /root/grpc/07-nested.txt 에 customer.bytes=16진수 customer.name=이름 customer.tier=숫자 세 줄을 적으세요.
8. decode_packed_varints(raw) -> [int, ...] 를 더하고 /opt/app/grpc/packed.bin 의 필드 7(packed repeated int32)을 풀어 /root/grpc/08-packed.txt 에 tags=쉼표목록 packed_record=16진수(필드 7 레코드 전체) expanded_record=16진수(원소마다 태그를 붙인 형태) 세 줄을 적으세요.
참고
- 규칙의 원본은 [Protocol Buffers 인코딩 문서](https://protobuf.dev/programming-guides/encoding/)입니다. 태그는
(field_number << 3) | wire_type, 와이어 타입은 0 VARINT · 1 I64 · 2 LEN · 5 I32. - 참조 구현
/opt/app/grpc/pbmini.py가 있습니다. 막히면 읽되, 이 실습의 채점기는 여러분의/root/grpc/pb.py를 불러와 지시문에 없는 무작위 값으로도 돌립니다. 답을 박아 두면 그 자리에서 떨어집니다. - 픽스처 바이트는
python3 -c "print(open('/opt/app/grpc/order_v1.bin','rb').read().hex())"로 볼 수 있습니다. - 흔한 실수:
decode_varint가pos인자를 무시하는 것, 문자열 길이를 글자 수로 세는 것(UTF-8 바이트 수여야 합니다), 음수int32를 32비트로 자르는 것(64비트입니다).
단계 8개
- varint 를 만들고 읽는다
- 태그에 번호와 와이어 타입을 담는다
- 음수 int32 는 10바이트, sint32 는 한두 바이트
- 필드 하나를 레코드로 만든다
- 바이트를 레코드 목록으로 되돌린다
- 모르는 필드를 건너뛰되 잃지 않는다
- 서브메시지는 LEN 안의 또 다른 메시지
- packed repeated 를 푼다