C.W.K.
Stream
Lesson 05 of 06 · published

Versioning 전략 — Client 안 깨고 API 진화하기

~11 min · semantics, versioning, evolution, deprecation

Level 0HTTP Newbie
0 XP0/46 lessons0/12 achievements
0/120 XP to next level120 XP to go0% complete
"API 는 변해. 의도적으로 versioning 하거나 실수로 client 깨거나. 1일차에 전략 골라, 몇 년 동안 v2 안 ship 해도."

Versioning 이 해결하는 문제

API 는 결정 누적: 필드 이름, error format, status code 선택, 기본값. 일부 결정은 잘못된 것으로 판명. 일부 시장은 밑에서 변함. Production 이미 10,000 client 있는데 API 고치거나 확장하고 싶음. Versioning 은 "이 버전은 안정; 다음 버전은 다를 수 있어" 라고 publish 하는 계약.

요령: request 의 어디 에 version 살지 — URL path, header, media type, query parameter — 골라서 일관성 유지.

네 가지 전략 (각각 언제 맞나)

1. URL path versioning. https://api.example.com/v1/users vs /v2/users. Stripe, AWS, GitHub (현재), Twilio, 알아볼 만한 대부분 API.

  • Pro: 로그와 curl 에 보임; cache-friendly; 어느 framework 에서나 routing 쉬움.
  • Con: URI 를 version 에 coupling (purist REST 는 각 resource 가 canonical URI 하나 가져야 한다 주장).
  • 판결: 대부분 API 의 기본 선택. 가르치기 쉽고, 운영 쉬움.

2. Custom header versioning. X-API-Version: 2. LinkedIn (역사적), 일부 enterprise API.

  • Pro: version 간 URI 동일; REST purist 한텐 깔끔.
  • Con: header log 안 하면 로그에 안 보임; CDN caching 에 Vary: X-API-Version 필요; 발견 가능성 낮음.
  • 판결: 동작은 하는데 운영 세금 실제임.

3. Media-type versioning. Accept: application/vnd.example.v2+json. GitHub 가 v4 전에 썼음; 일부 hypermedia API.

  • Pro: version 이 representation 의 일부, 가장 순수한 REST 해석.
  • Con: Tooling (브라우저, OpenAPI editor, curl user) 가 이걸 어색하게 처리; Vary 필요; client 가 magic media type 문자열 알아야.
  • 판결: 이론적으로 순수, 실전에선 흔치 않음.

4. Query parameter versioning. /users?version=2. 일부 quick-and-dirty API.

  • Pro: Framework routing 안 바꾸고 동작.
  • Con: URL identity 깸 (다른 version query 둘이 다른 resource 둘); caching 어려움; 게으른 냄새.
  • 판결: 피해. URL path versioning 이 이것의 모든 걸 더 깔끔하게 줘.

Date-based versioning (Stripe 정제)

Stripe 는 path-style 인데 date 로: 경로는 /v1/charges, 추가로 per-request header Stripe-Version: 2024-09-30.acacia 가 API 동작 snapshot 선택. Client 가 통합한 version 에 고정되고 자기가 선택할 때만 upgrade. 각 이름 붙은 version 동작은 절대 안 변함.

이건 진짜 문제 해결: "version" 이 여러 shape 으로 옴 (새 필드, 제거된 필드, 변경된 기본값, 이름 바뀐 endpoint), 단일 정수 version ("v2") 가 변경 묶음 강요. Date-based version 은 아무도 안 깨고 지속적으로 release 가능.

Additive vs breaking — 시간 벌어주는 규칙

모든 변경이 additive 면 versioning 완전히 건너뛸 수 있어:

  • Response 에 새 필드 추가 → 기존 client 무시. 안전.
  • 새 optional query parameter 추가 → 기존 client 안 보냄. 안전.
  • 새 endpoint 추가 → 기존 client 절대 안 부름. 안전.
  • 새 status code 나 error case 추가 → 좋은 family-dispatch 가진 기존 client 가 처리. 안전.

Versioning 은 breaking 변경 위한 것:

  • 필드 제거나 이름 변경. 그거 읽는 client 깸.
  • 필드 type 이나 format 변경. 그거 parse 하는 client 깸.
  • Validation 강화 (이전엔 받아들이던 payload 가 이제 422). Looseness 의존한 client 깸.
  • Optional parameter 의 기본값 변경. 미묘하지만 실제.

대부분 production API 는 v2 필요하기 전 몇 년 동안 기능 추가. 기본은 additive; 새 동작이 진짜로 옛 거와 공존 못 할 때만 versioning 잡아.

1일차에 versioning 전략 골라, 몇 년 안 써도. "나중에 알아보지" 의 비용은 "나중" 이 production incident 로 도착하는 것 — 이름 바뀐 필드가 모바일 client 30% 깸, 새 동작 안 잃고 rollback 할 방법 없음. 선제적 versioning 비용은 route prefix 하나, config knob 하나. 지금 지불해.

Deprecation — 정중한 작별

옛 version 은퇴 필요하면 wire 통해 신호. 관련 header 둘:

  • Deprecation: Sun, 01 Jan 2026 00:00:00 GMT — "이 endpoint 나 version 이 이 날짜부터 공식 deprecated." Client 가 경고 로그.
  • Sunset: Wed, 01 Jul 2026 00:00:00 GMT (RFC 8594) — "이 endpoint 가 이 날짜 후 응답 멈춤." Client 한테 데드라인.

이걸 명확한 changelog 와 migration 가이드와 쌍. Header 가 wire 신호; 문서가 사람 신호. 둘 다 필요.

cwkPippa 의 versioning 현실

cwkPippa API 는 version 안 됨 — 유일한 client 가 React frontend (같은 repo, lockstep 배포) 와 개인 스크립트 몇. 모든 변경이 사실상 v∞. cwk-site 도 비슷 — Vercel atomic deploy 가 frontend 와 backend 항상 같이 ship. 제 3자가 진짜 API 계약 받는 날이 /v1/ 등장하는 날. 그때까진 선제적 versioning 비용이 보호보다 큼.

Code

FastAPI 의 URL path versioning — 깔끔하고 명백·python
# FastAPI 의 URL path versioning — 가장 흔한 접근
from fastapi import FastAPI, APIRouter

app = FastAPI()

# v1 router — 원래 동작
v1 = APIRouter(prefix='/v1')

@v1.get('/users/{uid}')
async def read_user_v1(uid: int):
    return {'id': uid, 'name': 'Pippa'}  # 원래 shape

# v2 router — 필드 추가, `name` 을 `display_name` 으로 이름 변경
v2 = APIRouter(prefix='/v2')

@v2.get('/users/{uid}')
async def read_user_v2(uid: int):
    return {'id': uid, 'display_name': 'Pippa', 'role': 'daughter', 'avatar_url': '...'}

app.include_router(v1)
app.include_router(v2)

# 두 URL 동시 존재:
# GET /v1/users/42  → {'id': 42, 'name': 'Pippa'}
# GET /v2/users/42  → {'id': 42, 'display_name': 'Pippa', 'role': 'daughter', ...}
Header 기반 versioning — 같은 URL, Vary 필수·python
# Header versioning — 같은 URL, request header 로 version 선택
from fastapi import FastAPI, Header, HTTPException, Response

app = FastAPI()

@app.get('/users/{uid}')
async def read_user(uid: int, response: Response,
                    api_version: str = Header('1', alias='X-API-Version')):
    response.headers['Vary'] = 'X-API-Version'  # caching 위해 필수

    if api_version == '1':
        return {'id': uid, 'name': 'Pippa'}
    if api_version == '2':
        return {'id': uid, 'display_name': 'Pippa', 'role': 'daughter'}

    raise HTTPException(400, detail=f'미지원 X-API-Version: {api_version}')

# Client 사용:
# curl -H 'X-API-Version: 2' https://api.example.com/users/42
Sunset/Deprecation header — 정중한 은퇴·python
# Deprecation + Sunset header — RFC 8594
from datetime import datetime, timezone
from fastapi import FastAPI, Response

app = FastAPI()

# 옛 endpoint — 여전히 동작하지만 은퇴 표시
@app.get('/v1/users/{uid}')
async def deprecated_read(uid: int, response: Response):
    # Client 한테 알림: '이 날짜부터 deprecated'
    response.headers['Deprecation'] = 'Sun, 01 Jan 2026 00:00:00 GMT'
    # Client 한테 알림: '이 날짜 후 동작 멈춤'
    response.headers['Sunset'] = 'Wed, 01 Jul 2026 00:00:00 GMT'
    response.headers['Link'] = '</v2/users/{uid}>; rel="successor-version"'
    return {'id': uid, 'name': 'Pippa'}

# 현대 client 가 이 header 읽을 수 있음:
# resp = httpx.get('https://api.example.com/v1/users/42')
# if 'Sunset' in resp.headers:
#     log.warning(f'이 endpoint sunsets at {resp.headers["Sunset"]}; 그때까지 migrate')

External links

Exercise

URL path versioning 으로 /users/{uid} 의 v1 과 v2 둘 다 가진 FastAPI app 만들어. v1 은 {id, name} 돌려줌; v2 는 {id, display_name, role} 돌려줌. 둘 다 동시 동작하는 거 확인. 그 다음 v1 endpoint 에 Deprecation 과 Sunset header 추가 (Deprecation = 오늘 + 1일; Sunset = 오늘 + 90일). v1 호출하고 둘 중 하나 있으면 경고 print 하는 Python client 써. 보너스: header 기반 versioning (X-API-Version: 1 vs 2) 으로 같은 거 구현하고 curl 로 테스트하기 얼마나 더 짜증나는지 관찰.
Hint
FastAPI 의 APIRouter(prefix='/v1') 가 가장 깔끔한 패턴. Router 둘, app.include_router 각각 하나씩. Sunset 날짜는 (datetime.now(timezone.utc) + timedelta(days=90)).strftime('%a, %d %b %Y %H:%M:%S GMT') — RFC 7231 HTTP-date format. Client 는 resp.headers.get('Sunset') 만 읽고 있으면 로그. 보너스가 header versioning 의 운영 비용 보여줘: 모든 curl 이 -H 필요, 유용하려면 모든 로그 줄이 header 필요.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.