API 버전 관리 테스트: 거의 모든 팀이 놓치는 3가지 사례
요약
API 버전 관리 시 발생하는 하위 호환성 문제와 운영 환경에서의 장애 사례를 분석합니다. 단순히 새 버전을 만드는 것을 넘어, 버전 간 상호작용과 계약 검증을 포함한 테스트 전략의 중요성을 강조합니다.
핵심 포인트
- API 버전 관리는 단순히 새 버전을 만드는 것이 아닌 하위 호환성 보장이 핵심임
- 라우팅 변경 시 이전 버전 클라이언트가 새 버전 응답을 받지 않도록 검증 필요
- 버전 간 계약 검증(Contract Validation)을 위한 테스트 설계가 필수적임
- 이중 버전 계약 테스트를 통해 두 버전의 응답 일관성을 동시에 확인해야 함
수요일에 우리의 v2 API가 운영 환경(production)에서 v1 클라이언트들을 망가뜨렸습니다. 로드 밸런서(load-balancer) 라우팅 규칙이 수정된 후, v1 클라이언트가 v2 형태의 응답을 받게 되는 상황을 아무도 테스트하지 않았기 때문입니다.
장애는 한 시간도 채 지속되지 않았습니다.
하지만 그 피해는 훨씬 더 오래 지속되었습니다.
한 모바일 애플리케이션이 다음과 같은 필드명을 기대하고 있었기 때문에 조용히 실패하기 시작했습니다:
{
"customerName": "John Smith"
}
하지만 갑자기 다음과 같은 응답을 받았습니다:
{
"fullName": "John Smith",
"customerStatus": "Active"
...
기술적으로 v2 API에 잘못된 점은 없었습니다.
문제는 일부 v1 클라이언트들이 예상치 못하게 v2 응답을 받기 시작했다는 것이었습니다.
라우팅 변경은 배포 당시에는 무해해 보였습니다.
하지만 그 시나리오를 다루는 테스트는 없었습니다.
이 경험은 우리에게 중요한 교훈을 주었습니다:
API 버전 관리(Versioning)는 단순히 /v2를 만드는 것만이 아닙니다.
그것은 모든 버전이 소비자가 기대하는 대로 정확하게 계속 동작하도록 보장하는 것에 관한 것입니다.
대부분의 팀은 각 버전에 대해 독립적으로 해피 패스(happy-path) 테스트를 작성합니다. 버전 간의 상호작용, 하위 호환성(backward compatibility) 보장, 그리고 의도치 않은 교차 버전 동작(cross-version behavior)을 검증하는 테스트를 작성하는 팀은 매우 드뭅니다.
다음은 거의 모든 팀이 놓치는 **API 버전 관리 테스트 (API versioning tests)**의 세 가지 카테고리입니다.
버전 관리 테스트가 그 어느 때보다 중요한 이유
현대의 API는 끊임없이 진화합니다.
새로운 버전은 다음과 같은 것들을 도입합니다:
- 추가 필드 (Additional fields)
- 이름이 변경된 속성 (Renamed properties)
- 새로운 인증 요구사항 (New authentication requirements)
- 성능 개선 (Performance improvements)
- 비즈니스 로직 변경 (Business logic changes)
이 중 어느 것도 본질적으로 나쁜 것은 아닙니다.
위험은 다음과 같은 가정에서 발생합니다:
"v2가 작동하니까, v1도 여전히 작동할 것이다."
그 가정이 많은 운영 환경(production) 사고를 일으킵니다.
소비자들은 즉시 업그레이드하지 않습니다.
일부 클라이언트는 몇 달 또는 몇 년 동안 이전 버전을 계속 사용할 수도 있습니다.
단 한 번의 파괴적 변경(breaking change)이 다음과 같은 것들에 영향을 미칠 수 있습니다:
- 모바일 애플리케이션 (Mobile applications)
- 파트너 통합 (Partner integrations)
- 제3자 SDK (Third-party SDKs)
- 내부 마이크로서비스 (Internal microservices)
- 보고 시스템 (Reporting systems)
이것이 바로 **API 버전 테스트 (API version testing)**가 엔드포인트 수준의 검증 이상의 것을 요구하는 이유입니다.
그것은 버전 간의 계약 검증(contract validation)을 요구합니다.
1. 이중 버전 계약 테스트 (The Dual-Version Contract Test)
이것은 우리의 라우팅 사고(routing incident) 이후에 추가한 가장 가치 있는 테스트입니다.
개념은 간단합니다.
두 버전 모두에 존재하는 모든 요청은 두 버전에 대해 동시에 테스트되어야 합니다.
예를 들어:
GET /customers/123
은 다음을 대상으로 실행됩니다:
/v1/customers/123
/v2/customers/123
목표가 반드시 응답을 동일하게 만드는 것은 아닙니다.
목표는 계약적 보장(contractual guarantees)을 검증하는 것입니다.
테스트가 확인하는 사항
테스트는 다음 질문에 답할 수 있어야 합니다:
- v1이 여전히 약속된 모든 필드(field)를 반환하는가?
- 필드 타입(field types)이 변경되지 않았는가?
- 필수 속성(required properties)이 여전히 존재하는가?
- 지원 중단된(deprecated) 속성이 여전히 사용 가능한가?
- 상태 코드(status codes)가 일관적인가?
이를 통해 구현 변경 사항이 의도치 않게 이전 버전에 영향을 미치는 상황을 포착할 수 있습니다.
교차 버전 라우팅 테스트 (The Cross-Version Routing Test)
이제 대부분의 팀이 절대 테스트하지 않는 시나리오가 나옵니다.
만약 다음과 같은 상황이 발생하면 어떻게 될까요:
v1 요청 → v2 서비스
또는
v2 요청 → v1 서비스
이것은 일어날 것 같지 않게 들립니다.
하지만 분산 시스템(distributed systems)에서는 사람들이 예상하는 것보다 더 자주 발생합니다.
예시는 다음과 같습니다:
- 로드 밸런서(Load-balancer) 설정 오류
- 카나리 배포 (Canary deployments)
- 서비스 디스커버리 (Service discovery) 문제
- 프록시(Proxy) 규칙
- API 게이트웨이(API gateway) 변경
여러분의 시스템은 다음 중 하나를 수행해야 합니다:
- 안전하게 기능을 계속 수행한다.
- 명확한 버전 오류를 반환한다.
절대로 예측 불가능하게 실패해서는 안 됩니다.
테스트 매트릭스 예시
| 요청 | 예상 응답 |
|---|---|
| v1 → v1 | 성공 |
| ... |
대부분의 팀은 처음 두 행만 테스트합니다.
마지막 두 행이 바로 사고가 발생하는 지점입니다.
2. 지원 중단된 필드 단언 (Deprecated-Field Assertions) (네, 그것들이 존재하는지 단언해야 합니다)
이 권장 사항은 종종 사람들을 놀라게 합니다.
많은 엔지니어들은 지원 중단된(deprecated) 필드들이 테스트에서 점진적으로 사라져야 한다고 믿습니다.
하지만 실제로는 그 반대가 사실인 경우가 많습니다.
만약 어떤 필드가 지원 중단(deprecated)으로 표시되었지만 여전히 계약(contract)의 일부로 남아 있다면, 여러분의 테스트는 해당 필드의 존재를 명시적으로 단언(assert)해야 합니다.
실제 예시
버전 1은 다음을 반환합니다:
{
"customerName": "John Smith"
}
버전 2는 다음을 도입합니다:
{
"fullName": "John Smith"
}
팀은 다음과 같이 표시합니다:
customerName
을 (사용 중단 (deprecated)) 상태로 표시합니다.
모든 것이 괜찮아 보입니다.
6개월 후, 한 엔지니어가 아무도 더 이상 사용하지 않을 것이라고 가정하고 다음과 같이 삭제합니다:
customerName
불행히도:
- 모바일 앱들이 여전히 이에 의존하고 있습니다.
- 파트너 연동 (partner integration) 시스템이 여전히 이를 파싱합니다.
- 여러 보고서가 여전히 이를 사용합니다.
운영 환경 (Production)이 중단됩니다.
더 나은 접근 방식
여러분의 테스트는 다음과 같이 명시적으로 검증해야 합니다:
expect(response.customerName)
.toBeDefined();
해당 필드가 (사용 중단 (deprecated)) 상태일지라도 말입니다.
(사용 중단 (Deprecation))의 의미는 다음과 같습니다:
"제거될 예정임."
다음과 같은 의미가 아닙니다:
"오늘 바로 제거해도 안전함."
만료 날짜 추가하기
가장 좋은 관행 (best practice)은 단언 (assertion)을 메타데이터와 결합하는 것입니다.
예를 들어:
deprecated:
field: customerName
remove_after: 2027-01-01
이제 테스트는 팀에게 다음과 같은 사항을 상기시켜 줄 수 있습니다:
- 필드가 여전히 존재함.
- (사용 중단 (Deprecation)) 기간이 활성화되어 있음.
- 제거 날짜가 다가오고 있음.
이는 통제된 마이그레이션 (migration) 프로세스를 만들어냅니다.
3. 헤더 기반 vs URL 기반 버전 테스트 패턴
API 버전 관리 전략은 매우 다양합니다.
가장 일반적인 두 가지 접근 방식은 다음과 같습니다:
URL 버전 관리 (URL Versioning)
/api/v1/orders
/api/v2/orders
헤더 버전 관리 (Header Versioning)
Accept: application/vnd.company.v1+json
Accept: application/vnd.company.v2+json
두 접근 방식 모두 서로 다른 테스트 전략을 필요로 합니다.
URL 기반 테스트
이 접근 방식은 비교적 간단합니다.
테스트 스위트 (test suites)는 다음과 같은 대상을 쉽게 지정할 수 있습니다:
/v1/v2/v3
계약 분리 (Contract separation)가 명확합니다.
하지만 라우팅 (routing) 문제가 중요해집니다.
다음 사항을 검증하는 테스트가 필요합니다:
- URL 재작성 (URL rewrites)
- 게이트웨이 (Gateway) 동작
- 리다이렉트 (Redirect) 처리
헤더 기반 테스트
헤더 기반 버전 관리는 추가적인 복잡성을 야기합니다.
동일한 엔드포인트 (endpoint):
GET /orders
가 헤더에 따라 완전히 다른 응답을 반환할 수 있습니다.
테스트는 다음을 검증해야 합니다:
- 버전 헤더 누락
- 잘못된 버전
- 기본 버전 (Default versions)
- 지원되지 않는 버전
- 버전 협상 (Version negotiation) 동작
우리가 선호하는 테스트 패턴
모든 엔드포인트(Endpoint)에 대해:
헤더 없음 (No header)
v1 헤더
v2 헤더
...
이는 종종 예상치 못한 동작을 드러냅니다.
예시:
- 조용한 폴백 (Silent fallbacks)
- 잘못된 기본값 (Incorrect defaults)
- 캐시된 응답 (Cached responses)
- 부적절한 콘텐츠 협상 (Improper content negotiation)
v1과 v2 응답 간의 스냅샷 차이 (Snapshot Diffs)
가장 간단하면서도 효과적인 버전 관리 기술 중 하나는 스냅샷 비교 (Snapshot comparison)입니다.
대표적인 요청의 경우:
GET /customers/123
다음 항목을 캡처합니다:
v1_response.json
v2_response.json
그런 다음 구조적 차이(Structural diffs)를 생성합니다.
차이(Diff) 예시
{
- "customerName": "John Smith",
+ "fullName": "John Smith",
...
이를 통해 다음 사항을 즉각적으로 확인할 수 있습니다:
- 추가된 필드 (Added fields)
- 제거된 필드 (Removed fields)
- 타입 변경 (Type changes)
- 구조적 변경 (Structural changes)
스냅샷이 중요한 이유
사람은 코드 리뷰 중에 작은 변화를 놓치는 경우가 많습니다.
예시:
- "123"
+ 123
이는 사소해 보일 수 있습니다.
하지만 일부 소비자(Consumer)에게 이는 중대한 결함(Breaking change)이 됩니다.
스냅샷 테스트는 이러한 차이를 즉시 잡아냅니다.
페이로드 스냅샷보다 계약 스냅샷(Contract Snapshots)이 더 나은 이유
가능한 한 정확한 값(Exact values)을 비교하는 것은 피하십시오.
다음 항목들을 비교하십시오:
- 필드 이름 (Field names)
- 타입 (Types)
- 필수 속성 (Required properties)
- 구조 (Structure)
이렇게 하면 의미 있는 변경 감지 능력은 유지하면서 노이즈를 줄일 수 있습니다.
실수로 발생한 결함(Breaking Changes)을 잡아내는 CI 게이트
가장 효과적인 버전 관리 테스트는 릴리스 기간에만 실행되지 않습니다.
이들은 지속적으로 실행됩니다.
모든 풀 리퀘스트(Pull request)는 하나의 질문에 답해야 합니다:
이 변경 사항이 실수로 이전의 계약(Contract)을 깨뜨리는가?
CI 게이트가 확인해야 할 사항
스키마 변경 (Schema Changes)
- 필수 필드 제거
- 데이터 타입 변경
- 열거형(Enum) 값 변경
응답 변경 (Response Changes)
- 지원 중단된(Deprecated) 필드 제거
- 상태 코드(Status codes) 변경
- 에러 구조(Error structures) 수정
버전 동작 (Version Behavior)
- 라우팅(Routing) 변경
- 헤더 처리 (Header handling)
- 콘텐츠 협상 (Content negotiation)
CI 파이프라인 예시
빌드 (Build)
↓
버전 계약 테스트 실행 (Run version contract tests)
...
프로세스는 간단합니다.
하지만 그 영향력은 엄청납니다.
반드시 잡아내야 하는 결함(Breaking Changes)
예시는 다음과 같습니다:
- customerId: integer
+ customerId: string
- status: Active
+ state: Active
- customerName
이러한 변경 사항들은 개발자들에게는 무해해 보일 수 있습니다.
하지만 API 소비자(Consumer)들에게는 재앙이 될 수 있습니다.
지속 가능한 버전 테스트 전략 구축하기
실질적인 전략은 보통 네 가지 계층을 포함합니다.
계층 1: 엔드포인트 테스트 (Endpoint Tests)
각 버전을 독립적으로 검증합니다.
계층 2: 계약 테스트 (Contract Tests)
버전 간의 호환성을 검증합니다.
계층 3: 스냅샷 차이 분석 (Snapshot Diffs)
구조적 변경 사항을 강조하여 보여줍니다.
계층 4: CI 게이트 (CI Gates)
실수로 인한 회귀(Regression)가 운영 환경(Production)에 도달하는 것을 방지합니다.
이러한 계층적 접근 방식은 버전 관련 장애를 극적으로 줄여줍니다.
또한 새로운 버전을 도입할 때 확신을 제공합니다.
팀들이 여전히 이러한 테스트를 놓치는 이유
버전 관리 문제가 발생하는 이유는 팀들이 엔드포인트(Endpoint) 관점에서 생각하기 때문인 경우가 많습니다.
소비자(Consumer)는 계약(Contract) 관점에서 생각합니다.
이 두 관점은 서로 다릅니다.
엔드포인트는 완벽하게 작동할 수 있지만, 여전히 소비자에게는 장애를 일으킬 수 있습니다.
버전 테스트의 목표는 단순히 다음과 같은 것이 아닙니다:
"엔드포인트가 200 OK를 반환하는가?"
진정한 목표는 다음과 같습니다:
"지원되는 모든 소비자가 안전하게 계속 작동할 수 있는가?"
이 질문에 답하기 위해서는 더 넓은 범위의 테스트가 필요합니다.
마치며
제가 목격한 가장 고통스러운 API 장애들은 서버가 다운되거나 데이터베이스가 실패해서 발생한 것이 아니었습니다.
그것들은 작은 호환성 가정(Compatibility assumptions) 때문에 발생했습니다.
이름이 바뀐 필드.
삭제된 속성(Property).
라우팅 규칙.
헤더 협상(Header negotiation) 버그.
이러한 변경 사항들은 즉각적으로 경보를 울리는 경우가 드뭅니다.
대신, 아무도 테스트하는 것을 잊어버린 통합(Integration) 기능들을 조용히 망가뜨립니다.
강력한 **하위 호환성 테스트 (Backward compatibility tests)**와 세심한 **폐기 테스트 (Deprecation testing)**는 이러한 위험을 극적으로 줄여줍니다.
버전 관리는 단순히 새로운 API를 도입하는 것만이 아닙니다.
그것은 이전 버전에 여전히 의존하고 있는 모든 클라이언트와의 신뢰를 지키는 일입니다.
만약 귀하의 팀이 여러 API 버전을 구축하거나 유지 관리하고 있다면, **버전 관리 API에 우리가 사용하는 계약 테스트 접근 방식 (Contract-testing approach)**을 채택함으로써 변화를 위한 훨씬 더 안전한 기반을 마련할 수 있습니다:
https://totalshiftshiftleft.ai/api-contract-testing
버전 관리 시스템 (versioned systems)에서는 가장 큰 피해를 주는 버그가 대개 아무도 테스트할 생각을 하지 못했던 버그이기 때문입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기