LabHub
배우기 러닝패스 코스

フィールド番号を変えたら、古いクライアントが黙って間違った値を読んだ

契約検査器を作る

LabHub 에서 이어서 보기

한국어 원문으로 표시합니다.

목표

.proto 를 비교해 와이어 호환성 위반을 한 줄씩 출력하는 protocheck.py 를 만듭니다. 규칙을 하나씩 더해 가며 픽스처 8쌍과 채점기가 숨겨 둔 쌍을 통과시키고, 마지막에는 디렉터리 단위로 돌리는 check-all.sh 를 만들어 CI 에 넣을 수 있는 형태로 마무리합니다.

왜 중요한가

앞 실습에서 번호 하나를 바꾸면 옛 클라이언트가 오류 없이 틀린 값을 읽는 것을 봤습니다. 그 변경은 코드 리뷰에서 눈에 잘 띄지 않습니다 — diff 에는 숫자 두 개가 바뀐 것뿐이고, 컴파일도 테스트도 통과합니다. 사람이 매번 잡아낼 수 있는 종류의 실수가 아닙니다. 그래서 계약 검사는 리뷰어가 아니라 스크립트가 해야 합니다. 검사기의 규칙은 전부 공식 문서의 "안전한 변경·안전하지 않은 변경" 목록에서 나오며, 여러분이 만드는 것은 그 목록을 기계가 읽게 옮긴 것입니다.

단계

  1. /root/grpc/compat/protocheck.py 를 만들고 python3 protocheck.py --dump <file.proto> 가 메시지별 필드를 <메시지> <번호> <타입> <이름> 으로, 예약을 <메시지> reserved <번호><메시지> reserved "<이름>" 으로 한 줄씩 출력하게 하세요. 주석(//, /* */)·공백·옵션([deprecated=true])은 무시하고, 9 to 11 은 세 번호로 펴고, 중첩 메시지는 Outer.Inner 로 부릅니다.
  2. python3 protocheck.py <old.proto> <new.proto> 에 첫 규칙을 넣으세요 — 옛 번호가 새 파일에 없고 reserved 도 아니면 REMOVED_NOT_RESERVED <메시지>.<이름>=<번호>. 위반이 있으면 exit 1, 없으면 마지막 줄에 OK 를 찍고 exit 0.
  3. 같은 이름이 다른 번호로 옮겨 갔으면 RENUMBERED <메시지>.<이름> <옛번호>-><새번호>.
  4. 같은 번호·같은 이름에 타입이 다르면 TYPE_CHANGED <메시지>.<이름>=<번호> <옛타입>-><새타입>, 같은 번호에 이름도 타입도 다르면 NUMBER_REUSED <메시지>#<번호> <옛이름>:<옛타입>-><새이름>:<새타입>.
  5. 옛 파일이 reserved 한 번호(범위 포함)를 새 파일이 필드로 쓰면 REUSED_RESERVED <메시지>#<번호> <이름>.
  6. 같은 번호·같은 타입에 이름만 다르면 WARN RENAMED <메시지>#<번호> <옛이름>-><새이름> 을 찍되 exit 코드에는 영향을 주지 마세요(경고만 있으면 OK · exit 0).
  7. /opt/app/grpc/compat/ 의 8쌍을 모두 돌려 /root/grpc/compat/07-report.txt<case> OK 또는 <case> FAIL 을 한 줄씩 적으세요.
  8. /root/grpc/compat/check-all.sh <디렉터리> 를 쓰세요 — 그 아래 old.protonew.proto 가 있는 하위 디렉터리마다 검사기를 돌려 <case>: OK / <case>: FAIL 을 찍고, FAIL 이 하나라도 있으면 exit 1.

참고

.proto 를 읽는 파서를 만든다

/root/grpc/compat/protocheck.py 를 만들고 --dump <file.proto> 가 필드를 <메시지> <번호> <타입> <이름>, 예약을 <메시지> reserved <번호> / <메시지> reserved "<이름>" 으로 한 줄씩 출력하게 하세요.

주석을 먼저 지우고(re.sub 두 번), message 이름 { 를 찾아 중괄호를 세며 짝을 맞춘 뒤 안쪽을 읽습니다. 안쪽에 또 message 가 있으면 재귀하되 이름은 Outer.Inner 로 붙입니다. 필드는 타입 이름 = 번호; 정규식 하나로 잡히고, 앞의 optional/repeated 는 버립니다.

reserved 9 to 11, 15; 는 쉼표로 나눈 뒤 to 가 있으면 range 로 폅니다. reserved "foo"; 는 이름 예약입니다.

채점기는 픽스처 말고도 주석·이상한 공백·범위 예약·중첩·옵션이 섞인 파일을 넣어 봅니다.

예약 없이 지운 번호를 잡는다

python3 protocheck.py <old> <new>REMOVED_NOT_RESERVED <메시지>.<이름>=<번호> 규칙을 넣으세요. 위반이 있으면 exit 1, 없으면 마지막 줄 OK · exit 0.

메시지별로 옛 파일의 번호를 하나씩 보며 새 파일에 있는지, 없다면 새 파일의 reserved 에 있는지 봅니다. 둘 다 아니면 위반입니다. remove_no_reservednested(Customer.tier) 가 떨어지고 safe_add·safe_reserved 는 OK 여야 합니다.

종료 코드: sys.exit(1) 은 위반이 하나라도 있을 때만, 없으면 print("OK") 뒤 0.

번호가 옮겨 간 것을 잡는다

RENUMBERED <메시지>.<이름> <옛번호>-><새번호> 규칙을 더하세요.

새 파일에서 이름 → 번호 사전을 먼저 만들면 한 줄로 끝납니다: 옛 이름이 그 사전에 있는데 번호가 다르면 위반. 이 경우 그 번호에 대한 다른 검사(REMOVED 등)는 건너뜁니다 — 하나의 원인에 하나의 줄이 좋습니다.

renumber 픽스처는 id 와 qty 두 줄이 나와야 합니다.

타입 변경과 번호 재사용을 잡는다

TYPE_CHANGED <메시지>.<이름>=<번호> <옛타입>-><새타입>NUMBER_REUSED <메시지>#<번호> <옛이름>:<옛타입>-><새이름>:<새타입> 규칙을 더하세요.

같은 번호가 양쪽에 있을 때 타입이 다르면: 이름이 같으면 TYPE_CHANGED, 이름도 다르면 NUMBER_REUSED 입니다. 번호가 새 파일에 남아 있으니 REMOVED 는 아닙니다.

중첩 메시지(Order.Item) 안의 타입 변경도 잡혀야 합니다 — 파서가 Outer.Inner 로 이름을 붙였다면 메시지별 비교가 그대로 통합니다.

예약된 번호를 꺼내 쓴 것을 잡는다

REUSED_RESERVED <메시지>#<번호> <이름> 규칙을 더하세요. 옛 파일의 reserved 범위(10 to 12)도 포함입니다.

새 파일의 필드 번호가 옛 파일의 reserved 집합에 있으면 위반입니다. 1단계에서 범위를 펴 두었다면 in 하나로 끝납니다. reuse_reserved 픽스처와, 채점기가 만드는 reserved 10 to 12 + memo = 11 쌍이 떨어져야 하고, 같은 파일에서 13 을 쓰는 것은 OK 입니다.

이름 변경은 경고만

같은 번호·같은 타입에 이름만 다르면 WARN RENAMED <메시지>#<번호> <옛이름>-><새이름> 을 찍되 exit 코드에는 영향을 주지 마세요. 경고만 있으면 OK · exit 0.

경고와 오류를 다른 목록에 모으고, 종료 코드는 오류 목록만 봅니다. 경고는 오류보다 먼저 찍고, 오류가 없으면 마지막에 OK 를 찍습니다. rename_only 는 WARN 한 줄 + OK, 채점기가 만드는 "이름 변경 + 타입 변경" 쌍은 WARN 과 TYPE_CHANGED 가 함께 나오고 exit 1 입니다.

픽스처 8쌍을 전부 돌린다

/opt/app/grpc/compat/ 의 8쌍을 모두 돌려 /root/grpc/compat/07-report.txt<case> OK 또는 <case> FAIL 을 한 줄씩 적으세요.

for d in /opt/app/grpc/compat/*/; do ...; done 으로 돌리고 종료 코드로 OK/FAIL 을 정합니다. 손으로 적지 마세요 — 채점기는 여러분의 스크립트를 다시 돌려 보고서와 대조하고, 진짜 답과도 대조합니다. 셋이 다 같아야 통과입니다.

디렉터리 단위 검사 스크립트

/root/grpc/compat/check-all.sh <디렉터리> 를 쓰세요 — old.protonew.proto 가 있는 하위 디렉터리마다 검사기를 돌려 <case>: OK / <case>: FAIL 을 찍고 FAIL 이 하나라도 있으면 exit 1.

인자로 받은 디렉터리를 훑되 두 파일이 다 있는 하위 디렉터리만 봅니다(파일이나 빈 디렉터리는 건너뜁니다). 실패 수를 세어 마지막에 exit 합니다. 채점기는 픽스처 디렉터리 말고도 이름이 다른 임시 디렉터리 두 개를 만들어 돌립니다 — 경로를 박아 두면 떨어집니다.

protocheck.py 의 위치는 이 스크립트가 있는 디렉터리 기준으로 잡으면($(dirname "$0")) 어디서 불러도 됩니다.