Skip to content
Published on

API 버저닝 & 진화 전략 완전 가이드 2025: Breaking Changes 없는 API 진화, Deprecation, Sunset

Authors

TL;DR

  • 버저닝 전략 4가지: URL Path (/v1/), Header (API-Version: 2024-01-01), Content Negotiation (Accept: application/vnd.api+json;v=1), Query (?version=1)
  • Stripe 방식 = 날짜 기반: 2024-04-15처럼 날짜로 버전. 가장 우아한 접근
  • GitHub 방식 = REST URL: /v3/repos. 단순하지만 메이저 변경마다 마이그레이션 필요
  • GraphQL = 버전 없음: 필드 단위 deprecation으로 진화. Twitter, GitHub, Shopify 채택
  • Sunset 헤더: RFC 8594. Sunset: Sat, 31 Dec 2025 23:59:59 GMT — 클라이언트에게 폐기 일정 알림

1. API 진화의 본질적 어려움

1.1 외부 API의 운명

"한 번 공개된 API는 영원히 살아있다." — Hyrum's Law

내부 코드는 마음대로 리팩토링할 수 있습니다. 외부 API는 다릅니다:

  • 수천~수백만 클라이언트가 의존
  • 모바일 앱은 사용자가 업데이트해야 새 버전
  • 통합된 비즈니스 시스템은 변경에 인색
  • "잠시 멈춤"이 매출 손실로 직결

1.2 변경의 종류

변경영향호환성
새 엔드포인트 추가없음
새 응답 필드 추가없음
선택 필드 추가없음
응답 필드 제거Breaking
필드 이름 변경Breaking
필드 타입 변경Breaking
필수 필드 추가Breaking
에러 코드 의미 변경Breaking
기본 동작 변경Breaking

규칙: 추가는 안전, 제거/변경은 위험.

1.3 Hyrum's Law의 잔혹함

"API에 충분한 사용자가 있으면, 어떤 관찰 가능한 동작도 누군가는 의존한다."

실제 사례:

  • 응답 필드 순서를 가정하는 클라이언트
  • 에러 메시지의 정확한 텍스트를 파싱하는 클라이언트
  • 부수 효과(예: ID가 순차적인 것)에 의존하는 클라이언트
  • 응답 시간이 일정하다는 가정

결론: "공식 문서에 없는 것은 바꿔도 된다"는 잘못입니다. 모든 관찰 가능한 동작이 API의 일부입니다.


2. 버저닝 전략 4가지

2.1 URL Path Versioning

GET /v1/users/123
GET /v2/users/123

장점:

  • 가장 명확
  • 디버깅 쉬움 (URL만 봐도 버전 보임)
  • 캐시 친화적 (다른 URL = 다른 캐시)

단점:

  • 메이저 변경마다 새 URL 필요
  • 각 버전을 별도 코드베이스로 유지
  • 클라이언트가 URL을 하드코딩

채택: GitHub (/v3/), Twitter, Stripe (URL은 /v1/이지만 실제는 헤더로)

2.2 Header Versioning

GET /users/123 HTTP/1.1
Stripe-Version: 2024-04-15

장점:

  • URL이 깨끗
  • 같은 리소스, 다른 표현
  • 점진적 마이그레이션 쉬움

단점:

  • 디버깅 어려움 (헤더 안 보이면)
  • 캐시 처리 복잡 (Vary 헤더)
  • 클라이언트 라이브러리가 헤더 자동 추가해야 함

채택: Stripe (가장 유명), Azure

2.3 Content Negotiation

GET /users/123 HTTP/1.1
Accept: application/vnd.example.user.v2+json

장점:

  • HTTP 표준 (Accept 헤더 활용)
  • 같은 URL로 다른 버전
  • HATEOAS와 잘 맞음

단점:

  • 복잡함
  • 클라이언트가 정확한 MIME 타입 알아야 함

채택: GitHub (선택적, Accept: application/vnd.github.v3+json)

2.4 Query Parameter

GET /users/123?version=2
GET /users/123?api_version=2024-04-15

장점:

  • 가장 단순
  • URL에 보임 (디버깅 쉬움)

단점:

  • "데이터의 일부"처럼 보임 (실제로는 메타데이터)
  • 캐시 처리 복잡

채택: 일부 단순 API

2.5 비교표

방식URL 깨끗디버깅캐시채택
URL Path⭐⭐⭐⭐⭐⭐GitHub, Twitter
Header⭐⭐⭐⭐⭐Stripe, Azure
Content Negotiation⭐⭐⭐⭐⭐⭐⭐⭐GitHub (옵션)
Query Param⭐⭐⭐⭐⭐⭐⭐단순 API

3. Stripe의 천재적인 날짜 기반 버저닝

3.1 핵심 아이디어

Stripe는 버전을 날짜로 표현합니다:

  • 2024-04-15 (해당 날짜의 API 동작)
  • 2023-10-16 (이전 동작)
  • 2020-08-27 (5년 전 동작)

모든 변경은 날짜로 식별됩니다.

3.2 클라이언트 사용

import stripe

# 계정 기본 버전 사용
stripe.api_version = "2024-04-15"

# 또는 요청별
stripe.Charge.create(
    amount=2000,
    currency="usd",
    api_version="2023-10-16"  # 옛 동작
)

3.3 Stripe의 비결 — 변환 레이어

서버에는 단 하나의 코드베이스 (최신 버전).

각 요청에 대해:

  1. 클라이언트 버전 확인
  2. 요청을 최신 버전으로 변환 (forward transform)
  3. 처리
  4. 응답을 클라이언트 버전으로 변환 (backward transform)
클라이언트 (v2020)[변환] → 최신 코드 → [변환]클라이언트 (v2020)

효과:

  • 5년 전 클라이언트도 동작
  • 코드는 최신 상태 유지
  • 새 기능은 즉시 모든 사용자에게 (옵트인)

3.4 변환 예시

v2020에서 v2024 변경: 응답에 description 필드 추가.

# 변환 함수
def transform_to_v2020(response):
    if "description" in response:
        del response["description"]  # v2020 클라이언트는 이 필드를 모름
    return response

v2020에서 v2024 변경: amount가 정수에서 객체로 변경.

def transform_to_v2020(response):
    if isinstance(response.get("amount"), dict):
        response["amount"] = response["amount"]["value"]
    return response

3.5 Stripe 변경 로그

각 버전 변경이 정확히 문서화됩니다:

2024-04-15

  • Added description field to Charge object
  • amount field type changed from integer to AmountObject
  • Default currency is now derived from account settings

Migration guide: ...

이 정도의 투명성이 신뢰의 핵심입니다.


4. GraphQL의 버전 없는 진화

4.1 핵심 철학

GraphQL은 버전을 사용하지 않습니다. 대신 필드 단위로 진화:

  • 추가: 새 필드 추가 — 기존 클라이언트는 모르므로 영향 없음
  • 삭제: @deprecated 마크 후 일정 시간 후 제거
type User {
  id: ID!
  name: String!
  
  # Deprecated 필드
  email: String @deprecated(reason: "Use 'emailAddress' instead. Will be removed 2025-12-31")
  emailAddress: String!
}

4.2 클라이언트가 정확히 요청

query {
  user(id: "123") {
    id
    name
    emailAddress  # 새 필드만 요청
  }
}

기존 클라이언트는 email을 계속 요청 → 작동. 새 클라이언트는 emailAddress 사용.

Over-fetching이 없다 = 새 필드 추가가 무료.

4.3 Deprecation 추적

GraphQL 서버가 사용 통계를 수집:

  • 어떤 클라이언트가 email을 사용 중인가?
  • 마지막 사용은 언제?
  • 안전하게 제거 가능한가?

Apollo Studio, Hasura Cloud가 이 기능 제공.

4.4 GraphQL의 한계

  • 모든 변경이 호환되는 건 아님 — 타입 변경, enum 값 제거 등은 여전히 breaking
  • 클라이언트 코드 생성 — 새 스키마로 다시 빌드 필요
  • 고급 도구 필요 — 사용 추적 등

채택: GitHub, Shopify, Twitter, Airbnb


5. Semantic Versioning과 API

5.1 SemVer 기본

MAJOR.MINOR.PATCH
v1.2.3
  • MAJOR: 호환성 깨짐 (breaking)
  • MINOR: 호환성 유지 + 새 기능
  • PATCH: 호환성 유지 + 버그 수정

5.2 라이브러리 vs API

라이브러리: SemVer 자연스러움.

  • npm install foo@^1.0.0 → 1.x.x 자동 업데이트

Web API: 적용 어려움.

  • 클라이언트가 자동 업데이트 못 함
  • "v1.2"와 "v1.3"의 차이는 의미 있지만, "v1.2.3"과 "v1.2.4"는 거의 의미 없음

현실: Web API는 보통 메이저 버전만 표시 (/v1, /v2).

5.3 SemVer의 한계

v2.0.0이 나오면 모든 사용자가 마이그레이션 필요. 점진적 진화가 어려움.

Stripe 방식 (날짜 기반) 또는 **GraphQL 방식 (버전 없음)**이 더 우아.


6. Deprecation과 Sunset

6.1 Deprecation 단계

  1. Announce: 블로그, 이메일, 변경 로그
  2. Mark in API: 응답 헤더 또는 필드
  3. Monitor: 사용 추적
  4. Reminder: 사용 클라이언트에 직접 알림
  5. Sunset: 폐기 (HTTP 410 Gone)

6.2 Deprecation 헤더

RFC 8594: HTTP Sunset 헤더

HTTP/1.1 200 OK
Sunset: Sat, 31 Dec 2025 23:59:59 GMT
Deprecation: Sat, 31 Dec 2024 23:59:59 GMT
Link: <https://api.example.com/docs/migration>; rel="deprecation"

의미:

  • Deprecation: 이미 deprecated (아직 작동)
  • Sunset: 폐기 일정
  • Link: 마이그레이션 가이드

6.3 Deprecation 메시지 (응답 본문)

{
  "data": {...},
  "warnings": [
    {
      "code": "DEPRECATED_FIELD",
      "message": "Field 'email' is deprecated. Use 'emailAddress' instead.",
      "documentation_url": "https://api.example.com/docs/v2#email-deprecation",
      "sunset_date": "2025-12-31"
    }
  ]
}

6.4 사용자에게 알리기

기술적:

  • 응답 헤더 (Sunset, Deprecation)
  • 응답 본문의 warnings 필드

커뮤니케이션:

  • 이메일 (등록된 개발자)
  • 블로그 / 변경 로그
  • 대시보드 공지
  • 직접 연락 (큰 사용자)

Stripe: 사용 중인 deprecated API에 대해 자동 이메일 발송.

6.5 Sunset 정책 예시

사용자Sunset 기간
무료 사용자6개월
유료 사용자1년
엔터프라이즈2년

큰 회사일수록 변경에 시간 필요.


7. Breaking Changes 회피 전략

7.1 Additive Changes 우선

잘못된 변경:

- "user_email"
+ "email"

올바른 변경:

+ "email"  // 새 필드 추가
  "user_email"  // 옛 필드 유지 (deprecated)

두 필드를 같이 반환. 클라이언트가 마이그레이션할 시간 줌.

7.2 새 엔드포인트 vs 기존 변경

Bad: 기존 /users 응답 형식 변경.

Good: 새 /v2/users 엔드포인트 또는 /users?format=new.

7.3 Default 값 신중히

// v1
{ "page_size": 20 }  // 기본값 20

// v2 — 50으로 변경?
{ "page_size": 50 }  // ❌ Breaking! (페이지네이션 동작 변경)

기본값 변경은 종종 breaking입니다.

7.4 Optional → Required는 Breaking

- email: string?  // optional
+ email: string   // required

기존 클라이언트가 email을 보내지 않으면 실패. 추가하지 마세요.

7.5 Enum 값 추가는 안전?

enum Status {
  ACTIVE,
  INACTIVE,
+ PENDING_REVIEW  // 새 값 추가
}

미묘함: 클라이언트가 enum을 switch로 처리하면 새 값에 대한 default 케이스가 필요. 있다면 안전, 없다면 미묘한 버그.

조언: enum 추가는 기술적으로 backward compatible이지만, 클라이언트 코드 검토 필요.


8. 실제 API의 진화 사례

8.1 Stripe — 날짜 기반의 우아함

  • 10년+ API 진화
  • 모든 변경에 정확한 날짜
  • 클라이언트는 자기 페이스로 업그레이드
  • 사용 통계 + 자동 알림

8.2 GitHub — REST에서 GraphQL로

  • REST v3: 2014년부터 운영
  • GraphQL v4: 2017년 출시
  • 두 API 병행 운영
  • 새 기능은 GraphQL 우선

교훈: 기존 API는 그대로 두고, 새 패러다임은 별도 시작.

8.3 Twilio — 메이저 버전 + 점진적 마이그레이션

  • /2008-08-01/, /2010-04-01/처럼 날짜 prefix
  • 하지만 메이저 변경은 새 prefix
  • 옛 버전은 수년간 유지

8.4 Slack — 점진적 deprecation

  • 빈번한 새 메서드 추가
  • Deprecation은 6개월~1년 공지
  • 사용자에게 직접 이메일

8.5 AWS — 거의 절대 깨지 않음

  • 2006년 출시된 S3 API가 여전히 작동
  • 새 기능은 추가만, 기존은 절대 변경 안 함
  • 결과: API가 일관성 없고 복잡하지만 호환성 완벽

9. 모범 사례 체크리스트

9.1 설계 단계

  • 버저닝 전략 결정 (URL/Header/날짜)
  • 명확한 SLA (몇 년 지원할 것인가?)
  • Deprecation 정책 문서화
  • 변경 로그 자동화

9.2 변경 시

  • Breaking change인가? (체크리스트로 확인)
  • Additive로 만들 수 있나?
  • 마이그레이션 가이드 작성
  • Sunset 헤더 추가
  • 사용자에게 공지
  • 사용 통계 모니터링

9.3 Sunset 시

  • 사용자 0명까지 대기
  • 마지막 알림
  • HTTP 410 Gone 응답
  • 코드 제거 (시간이 지난 후)

10. API 진화의 미래

10.1 OpenAPI 3.1 + JSON Schema

스키마를 통한 자동 호환성 검사:

  • API spec 변경 시 자동으로 breaking change 감지
  • 클라이언트 코드 자동 생성

10.2 AI 기반 마이그레이션

  • AI가 코드 분석 → 자동 마이그레이션 PR 생성
  • 변경 영향도 자동 분석

10.3 contract testing 표준화

  • Pact, Spring Cloud Contract 같은 도구
  • API 제공자와 소비자 간 계약 강제
  • CI에서 호환성 검증

퀴즈

1. 가장 흔한 Breaking Change는?

: 응답 필드 제거 또는 이름 변경입니다. 클라이언트가 그 필드를 사용 중이라면 즉시 깨집니다. 안전한 대안: 새 필드를 추가하고 기존 필드를 deprecated로 표시 (둘 다 반환). 일정 기간 후 제거. 다른 흔한 breaking changes: 필드 타입 변경(string → object), 필수 필드 추가, 기본값 변경, enum 값 의미 변경.

2. Stripe의 날짜 기반 버저닝의 장점은?

: (1) 점진적 마이그레이션 — 클라이언트가 자기 페이스로 새 버전 채택, (2) 단일 코드베이스 — 서버는 최신 버전만 유지, 변환 레이어로 옛 클라이언트 지원, (3) 명확한 변경 로그 — 각 날짜의 변경 사항이 정확히 문서화, (4) 테스트 용이 — 특정 날짜 버전을 명시적으로 테스트. 단점은 변환 레이어 구현이 복잡하다는 것.

3. GraphQL이 버전을 사용하지 않는 이유는?

: GraphQL은 클라이언트가 정확히 필요한 필드만 요청하므로, 새 필드 추가가 기존 클라이언트에 영향 없습니다 — 그들은 그 필드를 요청하지 않으니까. 필드 제거는 @deprecated로 마크하고, 사용 통계를 추적하여 안전하게 제거 가능. 결과: 버전 없이 영원히 진화하는 API. 단점은 모든 변경이 호환되는 건 아니라는 것 (타입 변경, enum 값 제거 등).

4. Sunset 헤더의 역할은?

: RFC 8594 표준 HTTP 헤더로, "이 리소스가 언제 폐기될 것인가"를 알려줍니다. Sunset: Sat, 31 Dec 2025 23:59:59 GMT. 클라이언트는 이 헤더를 보고 마이그레이션 일정을 자동으로 인식할 수 있습니다. Deprecation 헤더와 Link 헤더(마이그레이션 가이드)와 함께 사용. 자동화된 클라이언트가 안전하게 폐기 일정에 대응할 수 있게 합니다.

5. Hyrum's Law가 API 설계에 의미하는 것은?

: "API에 충분한 사용자가 있으면, 어떤 관찰 가능한 동작도 누군가는 의존한다." 즉, 공식 문서에 없어도 변경하면 안 됩니다. 응답 필드 순서, 에러 메시지의 정확한 텍스트, 응답 시간, ID의 순차성 등 모두가 "API의 일부"가 됩니다. 결론: (1) 처음부터 신중하게 설계, (2) 변경 시 모든 관찰 가능한 동작을 고려, (3) 의도하지 않은 동작은 명시적으로 "이 동작에 의존하지 마세요"라고 문서화.


참고 자료