파괴적 API 변경 설명
마지막 업데이트:
파괴적 API 변경이란, API의 게시된 계약에 대한 변경 중, 기존 계약에 맞게 올바르게 작성된 클라이언트가 클라이언트 측의 아무 변경 없이 새 계약에서 실패할 수 있게 만드는 모든 변경을 말합니다. 이 한 문장이 이론의 전부이며, 이 글의 나머지는 모두 그 귀결입니다: 어떤 변경이 해당되는지, 무해해 보이지만 그렇지 않은 유명한 함정은 무엇인지, 명세 업데이트가 배포되기 전에 어떻게 검토하는지, 버전 전략이 클라이언트에게 어떻게 시간을 사 주는지를 다룹니다. 두 개의 실제 문서에 적용하려면 이전 명세와 새 명세에 OpenAPI Diff를 실행하고 이 페이지 옆에서 판정을 읽으세요.
계약은 미래에 대한 약속이다
API 명세는 가벼운 의미의 문서가 아닙니다. 약속입니다. 명세가 GET /pets가 선택적 limit 매개변수를 받아들이고 문자열 name을 담은 객체로 200을 반환한다고 선언하면, 모든 클라이언트 작성자는 정확히 그 위에 구축합니다. 매개변수는 필요 없을 때 생략되고, 상태로 분기하며, 필드는 문자열로 읽힙니다. 클라이언트의 올바름은 약속을 기준으로 판단되므로, 파괴적이라는 정의는 비대칭입니다. 올바르게 작성된 기존 클라이언트가 실패할 수 있다면 그 변경은 파괴적입니다. 대부분의 클라이언트가 우연히 살아남더라도 말입니다. 이전 명세로 생성된 엄격한 SDK가 바뀐 타입 때문에 실패하면 충분합니다. 깨진 것은 클라이언트가 아니라 약속입니다.
이 틀은 변경이 "해당되는지"에 대한 대부분의 논쟁을 해결합니다. 엔드포인트를 제거하는 것은 파괴적입니다. 약속에 그 존재가 포함돼 있었기 때문입니다. 엔드포인트를 추가하는 것은 아닙니다. 어떤 기존 클라이언트도 자신이 절대 호출하지 않는 경로에 대해 스스로 약속한 바가 없기 때문입니다. 새 선택적 응답 필드는 안전합니다. 기존 파서가 알 수 없는 필드를 무시하기 때문입니다. 어떤 합리적인 형식에서도 추가 전용 출력은 리더를 무효화할 수 없습니다. 반대로 새 필수 입력은 기존의 모든 요청에 손을 뻗어, 그것을 생략한 요청을 무효화합니다. 검사는 언제나 같습니다. 어제의 유효한 트래픽을 내일의 규칙으로 재생하고, 무엇이 이제 실패하는지 묻는 것입니다.
실무에서 중요한 귀결이 두 가지 있습니다. 첫째, 계약은 게시된 것이지, 서버가 오늘 우연히 하는 일이 아닙니다. 트래픽을 엿봐서 클라이언트가 발견한 문서화되지 않은 필드는 약속된 것이 아니며, 그것을 제거하는 것은 무례하지만 엄밀히는 파괴적이 아닙니다. 이는 유지하려는 것만 문서화해야 한다는 주장입니다. 둘째, 계약은 그것이 서 있는 표준의 전체 표면을 포함합니다. RFC 9110이 고정한 HTTP 메서드와 상태 의미론은 약속의 일부이므로, 명세 파일이 상태의 의미를 전혀 언급하지 않더라도 200이나 404의 의미를 재정의하는 것은 계약 변경입니다.
고전적인 함정
운영 환경에서 가장 흔한 파괴는 필수 필드 추가입니다. 팀이 요청 본문에 필드를 추가하고, 새 비즈니스 규칙이 필요하다며 필수로 표시한 뒤 출시하면, 그 필드를 보내지 않는 기존 클라이언트 모두가 400을 받기 시작합니다. 안전한 버전은 지루합니다. 필드를 서버 측 기본값이 있는 선택 항목으로 추가하거나, 새 엔드포인트에서만 요구하는 것입니다. 두 번째 함정은 열거값 제거입니다. 새 UI가 더 이상 제공하지 않는다는 이유로 상태값, 분류, 정렬 순서가 폐지되는데, 저장된 클라이언트 데이터와 구버전 모바일 앱, 파트너 통합은 몇 년 동안 계속 그것을 보냅니다. 열거값은 계속 받아들이는 데 거의 비용이 들지 않습니다. 하나를 철회하는 것은 엔드포인트를 삭제하는 것처럼扱아야 합니다.
타입 변경은 세 번째 함정이며 좁히기와 넓히기의 위장을 동반합니다. 좁히기 — 문자열에서 열거로, 정수에서 더 작은 범위로, 자유로운 객체에서 고정된 형태로 — 는 이전에 유효하던 입력을 거부하며 겉보기에도 파괴적입니다. 넓히기는 더 미묘합니다. 응답 필드를 문자열에서 문자열 또는 null로, 정수 id를 문자열 id로 바꾸는 것은 관대해 보이지만 이전 타입용으로 구축된 모든 엄격한 파서와 생성된 SDK를 깹니다. 응답 쪽에는 거울 함정이 있습니다. 항상 존재하던 필드가 선택 항목이 되는 것입니다. 클라이언트는 그것을 무조건 읽습니다. 그것을 생략한 첫 응답은 다른 누군가의 코드베이스에서 null 포인터가 됩니다.
네 번째 함정은 의미론적이며, 어떤 비교 도구도 잡을 수 없습니다. 필드는 이름과 타입을 유지한 채 의미를 바꿉니다. 가격이 조용히 센트에서 달러로, 타임스탬프가 UTC에서 현지 시간으로, 정렬돼 있던 목록이 더 이상 정렬되지 않게 됩니다. 명세 diff는 비어 있고 모든 클라이언트가 틀립니다. 의미론적 파괴야말로 기계적 diff가 검토의 상한이 아니라 하한인 이유입니다. 아무도 깰 수 없는 변경을 치워, 사람의 주의가 단순히 깰 수도 있는 변경에 집중하게 하기 위해서입니다.
배포 전에 공급업체 명세 업데이트 검토하기
공급업체는 끊임없이 새 명세 버전을 게시하며, "SDK를 올리고 재배포"가 조용한 파괴가 운영 환경에 닿는 경로입니다. 규율 있는 검토는 15분이면 됩니다. 첫째, 두 문서를 확보하세요. 여러분의 배포가 오늘 실제로 돌아가는 명세와 후보 명세를 말합니다. 그리고 변경 로그를 읽는 대신 기계적으로 비교하세요. 변경 로그는 마케팅 문서이기 때문입니다. 기능은 나열하지만 제거는 나열하지 않습니다. OpenAPI Diff는 그 목록을 로컬에서 생성합니다. API가 내부용이고 그 명세가 낯선 사람의 호스팅 도구에 붙여 넣어져서는 안 될 때 중요합니다.
둘째, 파괴적 목록을 여러분 자신의 사용 현황에 대한 체크리스트로 처리하세요. 공급업체의 파괴 대부분은 여러분에게는 이론적입니다. 절대 호출하지 않는 엔드포인트의 제거, 절대 만들지 않는 리소스의 새 필수 필드 같은 것들입니다. 위험한 항목은 여러분의 트래픽과 교차하는 것들입니다. 그래서 검토에는 diff만이 아니라 여러분의 호출 목록이 필요합니다. 셋째, 문서화되지 않은 표면을 확인하세요. 분기하는 상태, 파싱하는 오류 형태, 읽는 헤더를 말합니다. 웹훅 소비자를 보호하는 것과 같은 습관 — 웹훅 서명 설명에 나오듯 페이로드 계약을 고정하고 테스트하는 것 — 은 다른 누군가가 통제하는 모든 계약에 적용됩니다.
마지막으로, 단계적으로 도입하세요. 스테이징 클라이언트를 새 버전으로 향하게 하고, 기록된 운영 요청을 재생하며(HAR 가이드가 규정하는 방식으로 처리된 정제된 캡처), 어떤 사용자보다 먼저 응답을 비교하세요. 공급업체가 사용 중단 기간이나 버전 고정을 제공한다면 받아들이세요. 가장 저렴한 파괴적 변경은 공급업체의 출시일이 아니라, 클라이언트가 준비된 뒤 여러분의 일정에 맞춰 채택하는 것입니다.
버전 전략: URL, 헤더, 아니면 절대 깨지 않기
파괴적 변경에 대한 고전적인 답은 버전 식별자이며, 고전적인 논쟁은 그것을 어디에 둘 것인가입니다. URL 버전 관리(/v1/, /v2/)는 명시적이고, 캐시에 친화적이며, 자명하게 라우팅할 수 있고, 모든 로그 줄에 보입니다. 그 대가는 API 전체를 한 번에 버전 관리하므로, 파괴적인 엔드포인트 하나가 모든 클라이언트를 마이그레이션으로 끌어들인다는 점입니다. 헤더 버전 관리 — 버전 헤더나 날짜가 있는 콘텐츠 협상 방식 — 는 URL을 깨끗하게 유지하고 개별 리소스를 버전 관리할 수 있지만, 보이지 않는다는 대가가 있습니다. 버전은 로그, 링크, 브라우저 바에서 사라지고, 디버깅은 고고학이 됩니다. 어느 쪽도 틀리지 않았습니다. 업계는 단지 URL의 대가를 더 기꺼이 지불합니다. 새벽 3시에는 운용 가능성이 우아함을 이기기 때문입니다.
더 깊은 전략은 버전을 거의 필요로 하지 않는 것입니다. 추가 전용 진화 — 절대 제거하지 않고, 절대 필수로 만들지 않고, 절대 타입을 바꾸지 않고, 선택적인 것만 추가하는 방식 — 는 단일 버전을 무기한 살려 두며, 가장 오래 살아남은 공개 API들이 실제로 그렇게 운용합니다. 처음부터 규율이 필요합니다. 관대한 입력 허용, 범위가 커질 수 있도록 첫날부터 문자열로 타입이 정해진 id, 확장될 수 있게 설계된 열거, 그리고 무언가를 철회하기 훨씬 전에 경고하는 사용 중단 절차가 그것입니다. semver.org에 성문화된 의미론적 버전 관리는, OpenAPI Initiative 자체의 명세가 의도적으로 게시자에게 맡긴 신호 계층을 제공합니다. 메이저 올림은 파괴를 알리고, 클라이언트는 그것에 고정할 수 있습니다.
어떤 전략을 선택하든, 그것은 메커니즘으로 뒷받침되는 사회적 계약으로서만 작동합니다. 버전은 선언된 기간 동안 실제로 나란히 돌아가야 하고, 사용 중단은 클라이언트 작성자가 볼 수 있는 곳(변경 로그, 응답 헤더, 큰 소비자에 대한 직접 통지)에 공지돼야 하며, 이전 버전의 사용량은 측정돼 "더 이상 아무도 쓰지 않는다"가 희망이 아니라 사실이 돼야 합니다. 게시됐지만 나란히 운용할 수 없는 버전은 전략이 아닙니다. 파괴의 이름 바꾸기일 뿐입니다.
비교 도구가 들어갈 자리
기계적 비교 도구는 두 순간에 자리를 얻습니다. 검토에서는 "무엇이 깨졌는가?"를 나란히 읽는 오후에서, 체크해 나갈 수 있는 목록으로 바꿉니다. 그리고 똑같이 유용하게, 부정을 증명합니다. 릴리스가 추가 전용이라고 주장할 때, 빈 파괴적 목록은 느낌이 아니라 증거입니다. 지속적 통합에서는 같은 비교가 명세를 건드리는 모든 풀 리퀘스트에서 실행돼, 프로젝트 정책이 요구하는 버전 올림이나 승인 없이 파괴적 변경이 도착하면 빌드를 실패시킬 수 있습니다. 분류표가 정책이며, 도구는 마감 시점에 그것이 잊히는 것을 거부할 뿐입니다.
그 한계가 워크플로의 나머지를 정의합니다. 비교 도구는 선언된 계약만 읽습니다. 동작, 성능, 의미론, 클라이언트가 의존하는 문서화되지 않은 특이 사항은 볼 수 없고, 병적으로 깊은 스키마는 영원히 펼치는 대신 요약합니다. 그래서 완전한 게이트는 페이지가 아니라 파이프라인입니다. 계약에는 기계적 diff, 동작에는 실제 클라이언트 호출을 재생하는 계약 테스트, 의미론에는 단계적 배포, 그리고 셋 모두 무언가를 놓치는 날을 위한 모니터링입니다. 그렇게 쓰면 diff는 네 계층 중 가장 저렴한 층입니다. 파괴가 줄곧 명세에 적혀 있었는데 아무도 문서를 비교하지 않았다는, 가장 부끄러운 종류의 장애를 지우는 15초입니다.
그것이 이 글이 실제로 주장하는 습관입니다. 명세는 보관하기 저렴하고 비교하기는 더 저렴합니다. 사고는 그렇지 않습니다. 소유하거나 소비하는 모든 계약의 게시된 모든 버전을 보관하고, 출시 전에 모든 후보를 운영 중인 버전과 비교하며, 파괴적 목록을 릴리스의 진짜 안건으로 다루세요. 이렇게 하는 팀은 변경이 누군가를 깨뜨릴 "만했는지" 논쟁하기를 멈춥니다. 출시 전부터 날짜가 찍혀 보관된 목록을 갖고 있기 때문입니다.