构建契约检查器
한국어 원문으로 표시합니다.
목표
두 .proto 를 비교해 와이어 호환성 위반을 한 줄씩 출력하는 protocheck.py 를
만듭니다. 규칙을 하나씩 더해 가며 픽스처 8쌍과 채점기가 숨겨 둔 쌍을 통과시키고,
마지막에는 디렉터리 단위로 돌리는 check-all.sh 를 만들어 CI 에 넣을 수 있는
형태로 마무리합니다.
왜 중요한가
앞 실습에서 번호 하나를 바꾸면 옛 클라이언트가 오류 없이 틀린 값을 읽는 것을 봤습니다. 그 변경은 코드 리뷰에서 눈에 잘 띄지 않습니다 — diff 에는 숫자 두 개가 바뀐 것뿐이고, 컴파일도 테스트도 통과합니다. 사람이 매번 잡아낼 수 있는 종류의 실수가 아닙니다. 그래서 계약 검사는 리뷰어가 아니라 스크립트가 해야 합니다. 검사기의 규칙은 전부 공식 문서의 "안전한 변경·안전하지 않은 변경" 목록에서 나오며, 여러분이 만드는 것은 그 목록을 기계가 읽게 옮긴 것입니다.
단계
/root/grpc/compat/protocheck.py를 만들고python3 protocheck.py --dump <file.proto>가 메시지별 필드를<메시지> <번호> <타입> <이름>으로, 예약을<메시지> reserved <번호>와<메시지> reserved "<이름>"으로 한 줄씩 출력하게 하세요. 주석(//,/* */)·공백·옵션([deprecated=true])은 무시하고,9 to 11은 세 번호로 펴고, 중첩 메시지는Outer.Inner로 부릅니다.python3 protocheck.py <old.proto> <new.proto>에 첫 규칙을 넣으세요 — 옛 번호가 새 파일에 없고 reserved 도 아니면REMOVED_NOT_RESERVED <메시지>.<이름>=<번호>. 위반이 있으면 exit 1, 없으면 마지막 줄에OK를 찍고 exit 0.- 같은 이름이 다른 번호로 옮겨 갔으면
RENUMBERED <메시지>.<이름> <옛번호>-><새번호>. - 같은 번호·같은 이름에 타입이 다르면
TYPE_CHANGED <메시지>.<이름>=<번호> <옛타입>-><새타입>, 같은 번호에 이름도 타입도 다르면NUMBER_REUSED <메시지>#<번호> <옛이름>:<옛타입>-><새이름>:<새타입>. - 옛 파일이 reserved 한 번호(범위 포함)를 새 파일이 필드로 쓰면
REUSED_RESERVED <메시지>#<번호> <이름>. - 같은 번호·같은 타입에 이름만 다르면
WARN RENAMED <메시지>#<번호> <옛이름>-><새이름>을 찍되 exit 코드에는 영향을 주지 마세요(경고만 있으면OK· exit 0). /opt/app/grpc/compat/의 8쌍을 모두 돌려/root/grpc/compat/07-report.txt에<case> OK또는<case> FAIL을 한 줄씩 적으세요./root/grpc/compat/check-all.sh <디렉터리>를 쓰세요 — 그 아래old.proto와new.proto가 있는 하위 디렉터리마다 검사기를 돌려<case>: OK/<case>: FAIL을 찍고, FAIL 이 하나라도 있으면 exit 1.
참고
- 규칙의 원본: proto3 — 메시지 타입 갱신, Proto 모범 사례.
- 픽스처:
safe_addremove_no_reservedrenumbertype_changereuse_reservedrename_onlynestedsafe_reserved. 각 디렉터리의new.proto주석에 무엇을 바꿨는지 적혀 있습니다. - 채점기는 픽스처 말고도 지시문에 없는 .proto 쌍을 임시로 만들어 여러분의 스크립트에 넣습니다. 픽스처 이름이나 결과를 박아 두면 그 자리에서 떨어집니다. 판정은 종료 코드와 출력만으로 합니다.
- 규칙의 우선순위: 이름이 새 파일 어딘가에 다른 번호로 있으면 RENUMBERED 로 보고하고 그 번호는 더 보지 않습니다. 번호가 새 파일에 남아 있으면 REMOVED 가 아닙니다.
- 파서는 앞 실습의 채점기처럼 관대해야 합니다 —
syntax = "proto3" ;도 읽혀야 합니다.
.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_reserved 와 nested(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.proto 와 new.proto 가 있는 하위 디렉터리마다 검사기를 돌려 <case>: OK / <case>: FAIL 을 찍고 FAIL 이 하나라도 있으면 exit 1.
인자로 받은 디렉터리를 훑되 두 파일이 다 있는 하위 디렉터리만 봅니다(파일이나 빈 디렉터리는 건너뜁니다). 실패 수를 세어 마지막에 exit 합니다. 채점기는 픽스처 디렉터리 말고도 이름이 다른 임시 디렉터리 두 개를 만들어 돌립니다 — 경로를 박아 두면 떨어집니다.
protocheck.py 의 위치는 이 스크립트가 있는 디렉터리 기준으로 잡으면($(dirname "$0")) 어디서 불러도 됩니다.