API 변경

호출자를 위한 API 버저닝 모범 사례

6분 분량

API 버저닝이란 계약을 변경한 후에도 예전 계약을 계속 작동하게 유지하는 관행이며, 그럼으로써 호출자는 여러분의 일정이 아니라 자기 자신의 일정에 맞춰 이전할 수 있게 된다. 이 문장에는 중요한 두 가지 결정이 담겨 있다. 무엇을 계약의 변경으로 볼 것인가, 그리고 예전 것을 얼마나 오래 작동하게 유지할 것인가. 버전 번호가 어디에 있는가는, 많은 버저닝 논쟁의 중심에 있는 문제이지만, 셋 중 가장 덜 중요하며 가장 올바르게 처리하기 쉬운 부분이다.

API는 언제 버전으로 관리해야 하는가

변경이 올바른 호출자를 망가뜨릴 때만 API를 버전으로 관리하라. 추가적인 변경, 새 필드, 새 엔드포인트, 새 선택적 파라미터는 버전이 필요 없다. 예전 계약에 맞춰 작성된 호출자는 계속 작동하고, 새 기능은 그저 그곳에 존재할 뿐이기 때문이다. 파괴적 변경은 버전이 필요하다. 대안은 호출자가 오류를 통해 그것을 알게 되는 것이기 때문이다. 추가적인 것들까지 포함해 모든 릴리스를 버전으로 관리하는 것은 호출자에게 버전이 소음이라고 가르치는 것이며, 그들은 정말로 중요한 공지를 더는 읽지 않게 된다.

실용적인 테스트는 파괴적 변경 글에 있는 것과 같다. 문서화된 동작에만 의존했던 호출자가 계속 작동하기 위해 무언가를 바꿔야 한다면, 그 변경은 버전이 필요하다. 그렇지 않다면 현재 버전 아래서 출시하고 체인지로그 항목을 써라.

어떤 API 버저닝 방식을 써야 하는가

호출자가 가장 쉽게 보고 설정할 수 있는 방식을 써라. 대부분의 공개 API에서 그것은 URL 경로 안의 버전이거나 날짜가 붙은 버전 헤더다. 네 가지 흔한 방식은 능력보다는 호출자에게 무엇을 요구하는가에서 차이가 나며, 그것이 선택의 올바른 근거다.

방식예시호출자가 해야 할 일사용하는 곳
URL 경로/v2/invoices이전할 때 URL을 바꾼다대부분의 공개 REST API
버전 헤더X-GitHub-Api-Version: 2022-11-28헤더를 보내거나, 기본값을 받아들인다GitHub
날짜가 붙은 계정 버전Stripe-Version: 2026-08-26요청마다, 또는 계정마다 날짜를 고정한다Stripe
쿼리 파라미터/invoices?version=2파라미터를 붙인다오래된 API. 오늘날 잘 선택되지 않는다
미디어 타입Accept: application/vnd.example.v2+json콘텐츠 타입을 협상한다원칙주의자들. 관리할 수 있는 호출자는 적다

URL 경로는 가시성이 가장 높고 유연성이 가장 낮다. 모든 호출자는 로그 한 줄만 읽어도 자신이 어느 버전에 있는지 알 수 있고, 버전 올리기는 찾아 바꾸기로 끝난다. 대가는 전체 표면이 한 번에 움직인다는 것이다. 모든 엔드포인트에 새 버전을 발행하지 않고는 하나의 엔드포인트의 계약만 바꿀 수 없으므로, 경로 버전은 드물고 규모가 커지는 경향이 있다.

버전 헤더는 URL을 안정적으로 유지하면서, 아무것도 보내지 않는 호출자를 위해 서버 쪽에서 기본값을 고를 수 있게 한다. 그것이 GitHub의 REST API 버전이 작동하는 방식이다. X-GitHub-Api-Version 안의 날짜로 이름 붙은 버전이며, 지원되는 가장 오래된 버전이 기본값이 되어 버전을 지정하지 않은 호출자도 망가지지 않는다. 대가는 버전이 URL 안에서는 보이지 않으며 새 클라이언트에서 잊히기 쉽다는 것이다.

날짜가 붙은 계정 버전은 헤더 방식에 한 가지를 더한 것이다. 버전이 계정에 묶여 저장되므로, 아무것도 보내지 않아도 모든 요청이 그것을 받는다. Stripe의 버저닝은 각 계정을 생성 시점의 버전에 고정하고, 요청은 Stripe-Version으로 그것을 덮어쓸 수 있다. 이것은 호출자에게 가장 친절한 방식이며, 운영하는 쪽에는 가장 손이 많이 가는 방식이다. 지원하는 모든 버전과 현재 버전 사이를 서버가 번역해야 하기 때문이다.

쿼리 파라미터와 미디어 타입은 둘 다 작동하지만, 각자 다른 방식으로 가시성 테스트에 실패한다. 쿼리 파라미터는 URL을 조립할 때 빠뜨리기 쉽고, 미디어 타입 버전은 호출자가 디버깅에 쓰는 거의 모든 도구에서 보이지 않는다. Stripe의 날짜 기반 방식은 날짜 접근법의 가장 잘 알려진 예이며, Stripe는 API 버전을 어떻게 관리하는가가 그 과정을 차근차근 보여준다.

실제로 API 버저닝은 어떻게 하는가

실제로 버전이란 이름 붙은 동작의 집합이며, 서버는 각 요청을 그중 하나에 대응시킨다. 어떤 방식이 이름을 운반하든 단계는 같다.

  1. 버전은 시맨틱 버전이 아니라 날짜나 정수로 이름 붙인다. 웹 API는 패키지가 아니다. 호출자는 URL의 마이너 버전을 고정할 수 없으므로, v2나 2026-08-26은 호출자가 필요로 하는 모든 것을 전달하는 반면, 시맨틱 버저닝 번호는 이 방식이 지킬 수 없는 호환성의 약속을 암시해버린다.
  2. 신경 쓸 필요가 없는 코드 경로에서 버전을 멀리 두어라. 버전은 가장자리에서 변환 계층을 선택해야 하며, 비즈니스 로직을 분기시켜서는 안 된다. 코드베이스를 통째로 두 벌 갖는 것이 바로 버전이 관리되지 않는 채로 남는 방식이다.
  3. 모든 버전에 기본값과 문서를 부여하라. 버전을 보내지 않는 호출자는 최신이 아니라 항상 지원되는 가장 오래된 버전을 받아야 한다. 그래야 고정하지 않은 클라이언트가 릴리스 당일에 망가지지 않는다. 각 버전에는 이전 버전에서 무엇이 바뀌었는지 말하는 페이지가 있다.
  4. 지원 기간을 정하고 그것을 공개하라. Google의 버저닝 가이드인 AIP-185는 충분히 공지된 합리적인 전환 기간을 요구하며, 베타 기능에도 180일을 권장한다. 기간을 정해 문서로 남기고, 버전마다 재협상하지 않고 그것을 적용하라.
  5. 버전을 엔드포인트와 같은 방식으로 은퇴시켜라. 기간이 지난 버전은 여느 비추천된 API와 같은 대우를 받는다. 발표, 모든 응답에 붙는 Sunset 헤더(RFC 8594), 아직 남아 있는 호출자에게 보내는 중간 리마인더, 그리고 지켜지는 제거일이다.

REST API에서 v1과 v2란 무엇인가

v1과 v2는 같은 서버가 동시에 지원하는 두 계약의 이름이다. v2가 존재하는 이유는 v1 안의 무언가가 호출자를 망가뜨리지 않고는 바꿀 수 없었기 때문이며, 그 변경은 새 계약 안으로 들어갔고 예전 것은 계속 작동했다. 번호 그 자체는 v2가 완성되었다거나 v1이 죽었다는 것을 전혀 의미하지 않는다. 둘 다 문서가 그렇게 말할 때만 참이 된다. 분기마다 v3가 나타난다면, 그것은 추가적인 변경이 버전으로 관리되고 있거나, 애초에 계약이 변화를 흡수하도록 설계되지 않았다는 신호다.

이것은 URL 경로 버전 관리 모델이며, 버전 번호는 호출자가 다이얼하는 세그먼트다. gRPC 서비스는 보통 같은 문제를 다른 방식으로 해결한다. 버전은 .proto 파일 자체 안의 패키지 이름에 산다. gRPC와 Protobuf는 그 차이와, 그곳에서 와이어 호환성이 URL의 형태가 아니라 필드 번호로 정의되는 이유를 다룬다.

버전 변경은 무엇을 알려야 하는가

버전 변경은 무엇이 부서지는지, 누가 영향을 받는지, 어떻게 이전하는지, 그리고 예전 버전이 얼마나 오래 계속 작동하는지를 알려야 한다. 이 항목은 다른 파괴적 변경 항목과 같은 형태에 지원 기간을 말하는 한 줄을 더한 것이다. 다음은 헤더로 버전이 관리되는 API를 위한 예시다.

API 버전 2026-11-01을 사용할 수 있습니다. 버전 2025-06-15는 2027년 11월 1일까지 지원됩니다. 2026-11-01의 새로운 점: GET /invoices는 이제 amount를 소수 형태의 문자열이 아니라 최소 단위의 정수로 반환합니다. 비추천되었던 customer_name 필드는 제거되고 customer 객체로 대체되었습니다. amount를 문자열로 파싱하고 있는 2025-06-15 사용 호출자에게 영향을 줍니다. 이는 2025년 6월 이전에 생성된, 버전을 고정하지 않은 클라이언트의 기본값입니다. 이전 방법: amount를 정수로 파싱하고, 이름은 customer.name에서 읽으세요. 준비가 되면 X-Api-Version: 2026-11-01을 고정하세요. 버전을 고정하지 않은 호출자에게는 아무것도 바뀌지 않습니다.

마지막 문장이 대부분의 독자를 그 자리에서 읽기를 멈추게 해주는 문장이며, 모든 버전 발표에 포함되어야 한다. 체인지로그 예시 페이지는 이런 방식으로 버전을 관리하는 API들의 항목을 모아두었으며, 좋은 것들과 그렇지 않은 것들의 차이는 대개 이 마지막 한 줄에 있다.

버전이 바뀌면 누가 통보받는가

예전 버전에 있는 모든 사람에게는 개별적으로, 그 외 모든 사람에게는 체인지로그로. 버전 변경은 “게시해뒀다”가 반드시 중요한 호출자를 놓치게 되는 경우다. 즉, 2년 전에 버전을 고정한 이후 릴리스 노트를 한 번도 읽지 않은 사람들이다. 사용량 데이터가 그들이 누구인지 답해준다. 통지는 그들의 코드가 있는 곳, 즉 응답 헤더와 계정 소유자에게 보내는 메시지에 도달해야 한다.

우리가 운영하는 루프에서는, 버전을 알리는 항목이 그것을 출시하는 pull request로부터 초안이 작성되고, 사람이 검토하며, 피드와 위젯에 발행되고, 그곳에서 버전이 관리되는 클라이언트가 그것을 JSON으로 읽을 수 있다. 그 변경을 요청했거나 그것이 고치는 버그를 신고한 위젯 피드백이 pull request가 닫는 GitHub issue가 된 사람은 누구든, 항목이 발행될 때 그 issue에서 통보받는다. 그 메커니즘은 어떤 항목이든 같으며, 버전 올리기는 그저 가장 영향이 큰 항목일 뿐이다.

FAQ

모든 API 변경이 새 버전을 받아야 하는가? 아니다. 파괴적 변경만이다. 추가적인 변경은 현재 버전 아래서 체인지로그 항목과 함께 출시된다. 추가적인 변경을 버전으로 관리하는 것은 호출자에게 버전을 무시하도록 가르친다.

URL 버저닝과 헤더 버저닝 중 어느 것이 더 나은가? URL 버저닝은 호출자가 보기에는 쉽고, 여러분이 조금씩 발전시키기는 어렵다. 헤더 버저닝은 그 반대다. 작은 클라이언트가 많은 공개 API에서는 URL 버저닝이 덜 실패한다. 변환 계층을 가진 대규모 API에서는 날짜가 붙은 헤더가 더 잘 확장된다.

동시에 몇 개의 버전을 지원해야 하는가? 지원 기간이 허락하는 한 최소한으로, 결코 무제한으로는 안 된다. 두세 개의 동시 버전이 보통이며, 그것을 넘어서면 대개 버전이 은퇴되지 않고 있다는 뜻이다.

버전을 지정하지 않은 요청은 무엇을 받아야 하는가? 지원되는 가장 오래된 버전이다. 기존의 고정하지 않은 클라이언트가 계속 작동하도록 하기 위함이며, 어떤 버전을 받았는지 알려주는 응답 헤더와 함께 반환된다.


이 글의 기술적 내용은 독립적으로 검토되지 않았습니다. 잘못된 부분이 있으면 알려 주세요. 바로잡겠습니다.

changeloop 관련 페이지: 개발자 문서, changelog 예시

changeloop
루프를 닫는 changelog를 만드는 팀입니다. 사용자가 무언가를 요청하면 팀이 전달하고, 요청한 사람은 알게 됩니다.