API 버저닝 전략, 정답이 있을까
URL 경로, 헤더, 쿼리 파라미터... API 버저닝을 직접 해보며 겪은 것들
클라이언트가 깨진 금요일 오후
금요일 오후 4시. 모바일 앱 팀에서 슬랙이 왔다. "API 응답 구조가 바뀐 것 같은데, 앱이 크래시나요." 내가 그날 아침에 배포한 API 변경 때문이었다. user 객체에서 name 필드를 firstName과 lastName으로 쪼갠 건데, 모바일 앱은 아직 name을 참조하고 있었다.
버저닝 없이 API를 변경한 대가. 핫픽스로 name 필드를 다시 추가하고, deprecated 표시를 달고, 모바일 팀에 "2주 안에 새 필드로 마이그레이션 해주세요"라고 요청했다. 이 사건 이후로 API 버저닝을 진지하게 고민하기 시작했다.
URL 경로 방식: /v1/users
가장 직관적인 방법. /api/v1/users, /api/v2/users 이렇게 URL에 버전을 넣는다. GitHub, Stripe, Twitter API가 이 방식을 쓴다.
장점은 명확하다. URL만 보면 어떤 버전인지 바로 안다. API 문서 작성도 쉽다. 근데 문제는, 버전이 올라갈수록 라우팅이 복잡해진다는 거다. v1, v2, v3 각각의 컨트롤러가 필요하고, 공통 로직은 공유하면서 다른 부분만 분기하려면 코드가 스파게티가 된다.
내 프로젝트에서 v1과 v2를 동시에 운영했는데, 버그가 v1에서 나면 v2에도 같은 수정을 해야 했다. 이 이중 유지보수가 생각보다 피곤했다. 3개월 후에 v1 트래픽이 전체의 4%였는데도 끌 수가 없었다. 그 4%가 큰 클라이언트 한 곳이었거든.
헤더 방식: Accept-Version
Accept: application/vnd.api+json;version=2 같은 형식으로 요청 헤더에 버전을 넣는 방법. URL이 깔끔하게 유지된다는 게 장점이다.
근데 실무에서 써보니까 불편한 점이 있었다. 브라우저에서 직접 API를 테스트할 때 헤더를 넣어야 한다. Postman이나 curl에서는 상관없는데, 팀원들이 브라우저 주소창에 URL 치고 "왜 안 돼요?" 하는 상황이 생겼다.
API 게이트웨이 설정도 복잡해진다. URL 기반 라우팅은 대부분의 프록시가 지원하는데, 헤더 기반 라우팅은 추가 설정이 필요하다.
쿼리 파라미터: ?version=2
/api/users?version=2 방식. 간단하고 구현이 쉽다. 근데 이건 캐싱에서 문제가 생긴다. CDN이 URL 기준으로 캐시하는 경우, 쿼리 파라미터가 다르면 별도로 캐시해야 한다. 설정 실수하면 v1 응답이 v2 요청에 캐시 히트되는 사고가 난다.
실제로 이 사고를 겪었다. (정확히는 11분 동안 v2 요청에 v1 응답이 나갔다.) 모니터링에 잡히기 전에 한 유저가 "데이터가 이상해요"라고 리포트해서 발견했다. 11분이 짧다고 생각할 수 있는데, 그 11분 동안 API를 호출한 요청이 약 4,300건이었다.
내가 지금 쓰는 방식
결국 URL 경로 방식으로 정착했다. 이유는 단순하다. 가장 많은 팀에서 이해하는 방식이라서. API 소비자(프론트엔드, 모바일, 외부 파트너)에게 "v2를 쓰세요"라고 말하는 게, "헤더에 이거 넣으세요"보다 훨씬 쉽다.
대신 버전 변경 정책을 엄격하게 가져간다. 필드 추가는 비파괴적 변경이라 버전을 올리지 않는다. 필드 삭제나 타입 변경은 무조건 버전업. 새 버전 배포 시 이전 버전은 최소 6개월 유지. 이 규칙을 API 문서 첫 페이지에 박아놓았다.
정답이 있냐고 물으면
없다. 팀 상황, API 소비자가 누구인지, 변경 빈도에 따라 다르다. 근데 한 가지 확실한 건, "버저닝 없이 가겠다"는 건 답이 아니라는 것. 그 금요일 오후의 교훈이 아직도 생생하다.
API는 한번 공개하면 마음대로 바꿀 수 없다. 이 사실을 설계 단계에서 받아들여야 한다. 나는 이걸 사고 치고 나서야 배웠다.