# API 버저닝: Breaking Changes 없이 서비스를 진화시키는 법
Table of Contents
화요일 오전 10시, 앱이 멈췄다
새로운 기능 배포를 마치고 커피 한 잔 하려는데, 고객지원팀 슬랙 채널에 불이 나기 시작합니다.
“지금 앱에서 로그인이 안 된다는데요?” “구버전 사용자들 다 튕깁니다!”
등줄기에 식은땀이 흐릅니다. 방금 배포한 v2 API 코드를 다시 봅니다. 서버는 정상입니다. 에러 로그도 없습니다. 그런데 왜?
알고 보니 v2 API에서 응답 필드명을 user_name에서 userName으로 바꿨는데, 구버전 앱들이 여전히 user_name을 찾고 있었던 거죠. 앱은 이미 사용자 폰에 깔려있고, 긴급 업데이트 심사는 하루가 꼬박 걸립니다.
이것이 바로 **Breaking Change(하위 호환성을 깨는 변경)**의 공포입니다.
API는 클라이언트와의 **약속(Contract)**입니다. 백엔드 개발자가 이 약속을 일방적으로 어기면, 그 대가는 고스란히 서비스 장애로 이어집니다. 오늘은 이 약속을 지키면서도 서비스를 발전시킬 수 있는 ‘API 버저닝’에 대해 이야기해보려 합니다.
Breaking Changes가 뭔가요?
쉽게 말해 **“클라이언트 코드를 수정하지 않으면 망가지는 변경”**입니다.
절대 하면 안 되는 변경 (Breaking)
- 필드 이름 변경 (
id→userId) - 필드 타입 변경 (
age: 20 → “20”) - 필수 파라미터 추가 (갑자기
phone필드를 필수로 요구함) - 엔드포인트 삭제
- 성공/에러 응답 구조 변경
안전한 변경 (Non-Breaking)
- 새로운 필드 추가 (기존 필드는 유지)
- 새로운 엔드포인트 추가
- 선택적(Optional) 파라미터 추가
문제는 비즈니스가 성장하다 보면, 필드명을 바꾸거나 구조를 뜯어고쳐야 할 때가 반드시 온다는 겁니다. 이때 필요한 게 바로 **버저닝(Versioning)**입니다.
대표적인 API 버저닝 전략 3가지
정답은 없습니다. 팀의 상황과 취향에 따라 선택하면 됩니다. 가장 많이 쓰이는 3가지 방법을 비교해봅시다.
1. URI 버저닝 (URI Versioning)
URL 경로에 버전을 명시하는 방법입니다. 가장 직관적이고 널리 쓰입니다.
GET /api/v1/users/123
GET /api/v2/users/123
- 장점:
- 브라우저에서 바로 테스트해보기 쉽습니다.
- 어떤 버전을 호출하는지 URL만 보면 바로 알 수 있습니다.
- 캐싱(Caching) 설정이 간편합니다.
- 단점:
- URL이 자원(Resource)을 나타낸다는 REST 원칙 관점에서는 조금 거슬릴 수 있습니다. (버전은 자원의 ID가 아니니까요)
- 코드상에서 라우팅 분기가 필요합니다.
2. 헤더 버저닝 (Header Versioning)
HTTP 헤더에 버전을 담아 보냅니다. URL은 깔끔하게 유지됩니다.
GET /api/users/123
Headers:
X-API-Version: 2
- 장점:
- URL이 깔끔해집니다 (
/api/users/123). - RESTful 철학에 더 가깝습니다.
- 단점:
- 브라우저 주소창에 쳐서 테스트하기가 번거롭습니다 (Curl이나 Postman 필요).
- 캐싱이 까다로울 수 있습니다 (헤더 값에 따라 캐시 키를 다르게 잡아야 함).
3. 미디어 타입 버저닝 (Accept Header)
GitHub가 사용하는 방식입니다. Accept 헤더를 활용해 데이터의 형식을 버전별로 다르게 요청합니다.
GET /api/users/123
Headers:
Accept: application/vnd.mycompany.v1+json
- 장점:
- 가장 ‘RESTful’하고 멋있어 보입니다.
- 버전뿐만 아니라 응답 포맷(JSON, XML 등)도 제어할 수 있습니다.
- 단점:
- 구현이 복잡합니다.
- 클라이언트 개발자가 헤더 만드는 걸 귀찮아할 수 있습니다.
실전 버저닝 패턴: Expand-Contract
버전을 올리는 것만이 능사는 아닙니다. 데이터베이스 스키마나 내부 로직을 변경할 때, 무중단으로 안전하게 배포하는 Expand-Contract (확장 후 축소) 패턴을 소개합니다.
예를 들어, name 필드를 fullName으로 바꾸고 싶다고 가정해봅시다.
1단계: 확장 (Expand)
기존 name 필드는 그대로 두고, fullName 필드를 추가합니다. 그리고 서버는 두 필드 모두에 값을 채워서 내려줍니다.
{
"name": "홍길동", // 구버전 호환용
"fullName": "홍길동" // 신규 필드
}
2단계: 이주 (Migrate)
클라이언트 개발자들에게 공지합니다. “이제 name 대신 fullName 쓰세요!”
구버전 앱 사용자들이 업데이트할 때까지 충분한 시간(보통 몇 달)을 기다립니다.
3단계: 축소 (Contract)
모니터링 결과 name 필드를 사용하는 트래픽이 거의 0에 수렴하면, 그때 name 필드를 제거합니다.
이 방식을 쓰면 굳이 v2를 만들지 않고도 v1 안에서 부드럽게 변경사항을 적용할 수 있습니다.
Deprecation 정책: 이별에도 예의가 필요하다
API 버전을 종료(Deprecate)할 때는 클라이언트에게 충분한 예고 기간을 줘야 합니다.
- 공지: “2025년 12월 31일부로 v1 API 지원을 종료합니다.”라고 메일도 보내고 문서에도 대문짝만하게 써놓으세요.
- 헤더 예고: 응답 헤더에 “너 지금 구버전 쓰고 있어”라고 알려주는 센스.
Warning: 299 - "This API version is deprecated. Please upgrade to v2."
- Brownout (정전 테스트): 종료일이 다가오면, 간헐적으로 v1 API를 5분, 10분씩 일부러 실패시켜보기도 합니다. (물론 사전 공지 필수!) 로그를 안 보던 개발자들도 에러가 나면 연락이 오거든요.
마치며
API 버저닝은 기술적인 문제이기도 하지만, 결국 커뮤니케이션의 문제입니다.
완벽한 설계는 없습니다. 처음부터 v1을 달고 시작하는 게 마음 편할 수도 있고, Stripe처럼 날짜 기반(2024-11-15) 버저닝을 할 수도 있습니다.
중요한 건 **“내가 만든 API를 사용하는 동료나 고객을 놀라게 하지 않는 것”**입니다. 예측 가능한 변경, 친절한 에러 메시지, 충분한 마이그레이션 기간. 이 세 가지만 기억해도 여러분은 훨씬 더 신뢰받는 백엔드 엔지니어가 될 수 있을 겁니다.