통합과 배포 · 말없이 바뀐 계약 · 이론
필드 하나가 이름을 바꿨는데 예외는 안 났다
한 줄 요약
남의 시스템과 붙을 때 우리가 지켜야 할 것은 그쪽의 전체 스키마가 아니라 우리가 실제로 읽는 필드의 목록이고, 그 목록을 문서가 아니라 매번 도는 시험으로 못박아 두어야 판올림이 조용히 우리 숫자를 틀리게 만들지 못한다.
왜 이게 필요했나
통합에서 가장 비싼 사고는 연결이 끊기는 사고가 아니다. 연결이 끊기면 경보가 울리고 사람이 달려온다. 진짜 비싼 것은 아무 오류 없이 틀린 숫자가 나오는 사고다.
파트너가 주문 API 를 1.3 에서 1.4 로 올렸다고 하자. 바뀐 것은 세 가지다. region 이 market 으로 이름을 바꾸고, amount 가 정수 12300 에서 문자열 "12300.00" 으로 바뀌고, status 에 on_hold 라는 값이 하나 늘었다. 우리 수집기는 어떻게 될까.
rec.get("region")은 예외를 내지 않는다.None을 돌려준다. 지역별 매출 보고서의 모든 행이 "미분류" 로 모인다.sum(r["amount"] for r in rows)는 파이썬이라면TypeError를 낸다 — 운이 좋은 경우다. 그런데 문자열끼리 이어 붙이는 코드거나 자바스크립트라면 조용히"1230012300…"이 된다.if status == "paid"로만 갈라 놓은 코드에서on_hold는 어느 갈래에도 안 들어가고, 그 주문은 집계에서 통째로 빠진다.
세 가지 모두 경보가 울리지 않는다. 몇 주 뒤 고객이 "숫자가 좀 이상한데요" 라고 말할 때, 이미 틀린 보고서는 여러 장 나간 뒤다.
어떻게 동작하나
이 문제를 다루는 방법은 셋이다.
첫째, 무엇이 깨뜨리는지 안다. 소비자(응답을 읽는 쪽) 관점에서 변경을 가르면 규칙은 단순하다. 필드가 더해지는 것은 안전하고, 사라지거나 이름이 바뀌거나 타입이 바뀌는 것은 안전하지 않다. 열거값이 느는 것도 안전하지 않다 — 우리 분기에 없는 값이 오면 어느 갈래로도 안 가기 때문이다. 방향이 반대인 요청 쪽은 규칙도 반대다. 요청에 선택 필드가 느는 것은 안전하지만 필수 필드가 느는 것은 우리 요청이 거절되므로 깨뜨린다.
여기서 기준이 되는 것이 "모르는 필드를 어떻게 할 것인가" 다. [JSON Schema 2020-12 의 core 명세](https://json-schema.org/draft/2020-12/json-schema-core)는 additionalProperties 로 그 처리를 정하고, [required](https://json-schema.org/understanding-json-schema/reference/object) 는 반드시 있어야 하는 키의 목록이다. 소비자 계약을 쓸 때는 대개 모르는 필드를 허용한다. 그래야 파트너가 필드를 더할 때마다 우리가 깨지지 않는다.
둘째, 계약을 우리 쪽에서 쓴다. 파트너의 [OpenAPI 명세](https://spec.openapis.org/oas/v3.1.0.html)를 그대로 우리 계약으로 삼고 싶은 유혹이 있는데, 그러면 우리가 안 읽는 필드의 변경에도 우리 시험이 빨간불을 켠다. 소음이 늘면 사람은 시험을 끈다. 그래서 계약에는 우리가 실제로 읽는 필드만 적는다. 이 방식을 흔히 소비자 주도 계약(consumer-driven contract)이라고 부른다.
셋째, 계약을 문서가 아니라 시험으로 둔다. 파트너의 응답을 받아 계약과 대조하고, 어긋나면 0 이 아닌 코드로 끝나는 스크립트 하나면 된다. 그것을 파이프라인에 넣으면, 파트너가 공지 없이 움직여도 우리가 먼저 안다. 판올림 공지 메일을 못 봤다는 것은 사고 보고서에 가장 자주 적히는 문장이다.
파트너 응답 ──▶ 계약 대조기 ──▶ 위반 0 ? 통과 │ └─ 위반 n ? 파이프라인 실패 + 무엇이 어긋났는지 필드 단위로 출력버전 표기 자체도 약속이다. [유의적 버전(Semantic Versioning)](https://semver.org/)은 호환을 깨는 변경에 주 번호를 올리라고 정한다. 다만 그것은 발행자의 약속이라 파트너가 지키지 않을 수도 있다. 1.3 에서 1.4 로 올라갔는데 우리가 깨졌다면, 그것은 우리가 잘못 읽은 것이 아니라 그쪽이 약속을 어긴 것이다 — 그런데 그 사실을 증명하려면 계약 시험의 출력이 필요하다.
현장에서 만나는 모습
첫째, 판올림은 한 번에 오지 않는다. 구판과 신판이 몇 달씩 함께 돈다. 그래서 읽는 쪽은 둘 다 받아들여야 한다. 이름이 바뀐 자리는 별칭 표로, 타입이 바뀐 자리는 정규화 함수로 흡수하고, 내부에서는 한 가지 모양만 쓴다. 별칭 표를 코드 곳곳에 흩어 놓으면 세 달 뒤 구판을 끊을 때 어디를 지워야 하는지 아무도 모른다.
둘째, "필수인데 가끔 빈다" 가 가장 많다. 명세에는 필수라고 적혀 있는데 실제 응답의 3%는 빈 문자열이다. 그래서 계약 시험은 명세를 읽는 것이 아니라 실제 응답을 표본이 아니라 전수로 훑어야 한다.
셋째, 금액과 시각이 늘 문제다. 정수 최소단위에서 십진 문자열로, 또는 그 반대로 바뀌는 일이 잦다. 부동소수로 받는 순간 반올림이 생기므로, 정규화는 정수나 문자열로 하고 실수를 거치지 않는다.
넷째, 계약 시험을 어디서 돌릴지가 실제 쟁점이다. 파트너의 운영 환경에 대고 매 분 돌릴 수는 없다. 보통은 그쪽이 주는 샌드박스에 대고 하루에 몇 번, 그리고 배포 직전에 한 번 돌린다. 샌드박스와 운영이 다른 판을 돌리고 있는 경우가 있으니, 계약 시험의 출력에 어느 주소에 대고 쟀는지를 반드시 남긴다.
다음 실습에서 할 것
파트너 주문 API 의 1.3 과 1.4 를 함께 내주는 서버를 띄워 두 판의 응답을 손에 쥔다. 필드 차이를 기계가 읽을 수 있는 형태로 뽑고, 변경 종류별로 무엇이 우리를 깨뜨리는지 판정하는 작은 도구를 만든다. 그다음 우리가 읽는 필드만 적은 소비자 계약과 그 계약을 대조하는 대조기를 만들고, 구판과 신판을 모두 받아들이는 읽기 계층을 붙인다. 마지막에는 파트너가 공지 없이 또 움직인 상황을 만들어, 계약 시험이 그것을 필드 단위로 잡아내는지 확인한다.