237 lines
5.5 KiB
Markdown
237 lines
5.5 KiB
Markdown
---
|
|
id: api-versioning-strategies
|
|
title: API Versioning — URL / Header / Date 전략
|
|
category: Coding
|
|
status: draft
|
|
source_trust_level: B
|
|
verification_status: conceptual
|
|
created_at: 2026-05-09
|
|
updated_at: 2026-05-09
|
|
tags: [api, versioning, vibe-coding]
|
|
tech_stack: { language: "TS", applicable_to: ["Backend"] }
|
|
applied_in: []
|
|
aliases: [API versioning, semver, breaking change, deprecation, sunset header]
|
|
---
|
|
|
|
# API Versioning
|
|
|
|
> Breaking change = client 깨짐. **URL versioning (`/v2`) 가 가장 단순**. Date versioning (Stripe) 이 정밀. **Sunset header + 6+ 개월 deprecation**.
|
|
|
|
## 📖 핵심 개념
|
|
- Breaking: 필드 제거, type 변경, 동작 변경.
|
|
- Non-breaking: 필드 추가, optional 추가, enum 값 추가 (cautious).
|
|
- Deprecation: 사용 가능 but 제거 예정.
|
|
- Sunset: 이 날짜에 제거.
|
|
|
|
## 💻 코드 패턴
|
|
|
|
### URL versioning (가장 일반)
|
|
```
|
|
GET /v1/orders
|
|
GET /v2/orders
|
|
```
|
|
|
|
```ts
|
|
app.use('/v1', v1Router);
|
|
app.use('/v2', v2Router);
|
|
```
|
|
|
|
```
|
|
장점: 간단, 캐시 친화, 명확.
|
|
단점: URL 변경 = client 코드 변경.
|
|
```
|
|
|
|
### Header versioning
|
|
```
|
|
GET /orders
|
|
Accept: application/vnd.acme.v2+json
|
|
```
|
|
|
|
```ts
|
|
app.get('/orders', (req, res) => {
|
|
const version = parseAccept(req.headers.accept);
|
|
if (version >= 2) return v2Handler(req, res);
|
|
return v1Handler(req, res);
|
|
});
|
|
```
|
|
|
|
```
|
|
장점: URL 깔끔.
|
|
단점: Cache 어려움 (Vary), 디버깅 어려움.
|
|
```
|
|
|
|
### Query versioning
|
|
```
|
|
GET /orders?api_version=2
|
|
```
|
|
|
|
→ 임시 / experimental 만. 공식 X.
|
|
|
|
### Date versioning (Stripe)
|
|
```
|
|
GET /orders
|
|
Stripe-Version: 2024-11-20
|
|
```
|
|
|
|
```
|
|
장점: 매우 정밀, account 별 lock.
|
|
단점: 복잡, transformation chain 유지.
|
|
```
|
|
|
|
```ts
|
|
// 변환 chain
|
|
const transforms: Record<string, (data: any) => any> = {
|
|
'2024-11-20': (d) => ({ ...d, oldField: d.newField }), // 옛 client
|
|
'2025-01-15': (d) => d, // current
|
|
};
|
|
|
|
// 응답 시
|
|
const clientVer = req.headers['stripe-version'] ?? '2025-01-15';
|
|
let data = currentResponse;
|
|
for (const v of versionsAfter(clientVer)) {
|
|
data = transforms[v](data);
|
|
}
|
|
return data;
|
|
```
|
|
|
|
### Backwards-compatible 변경
|
|
```ts
|
|
// ✅ 새 필드 추가
|
|
{ id, status, createdAt, refundedAt: '2026-05-09' } // refundedAt 새 — old client 무시
|
|
|
|
// ✅ Enum 값 추가 (cautious)
|
|
{ status: 'open' | 'paid' | 'shipped' | 'returned' } // 'returned' 새
|
|
|
|
// ❌ Type 변경
|
|
{ amount: 100 } → { amount: '100.00' } // breaking
|
|
|
|
// ❌ 필드 제거
|
|
{ id, status } → { id } // breaking
|
|
|
|
// ❌ 의미 변경
|
|
status: 'paid' (이전: 결제+배송 OK) → status: 'paid' (이전: 결제 OK 만)
|
|
```
|
|
|
|
### Deprecation header
|
|
```
|
|
HTTP/1.1 200 OK
|
|
Deprecation: true
|
|
Sunset: Sat, 31 Dec 2026 23:59:59 GMT
|
|
Link: <https://docs.acme.com/v2-migration>; rel="deprecation"
|
|
```
|
|
|
|
```ts
|
|
res.set('Deprecation', 'true');
|
|
res.set('Sunset', 'Sat, 31 Dec 2026 23:59:59 GMT');
|
|
res.set('Link', '<https://docs.acme.com/v2>; rel="deprecation"');
|
|
```
|
|
|
|
→ Client 가 검출 + 알림.
|
|
|
|
### Tracking version usage
|
|
```ts
|
|
app.use((req, res, next) => {
|
|
const version = req.path.startsWith('/v1') ? 'v1' : 'v2';
|
|
metrics.increment('api.calls', { version, endpoint: req.route?.path });
|
|
next();
|
|
});
|
|
```
|
|
|
|
→ 어떤 client 가 옛 version 인지 → email.
|
|
|
|
### 점진 migration
|
|
```
|
|
Phase 1 (출시): v2 출시, v1 그대로
|
|
Phase 2 (3개월): v2 권장 — docs 업데이트
|
|
Phase 3 (6개월): v1 deprecation header
|
|
Phase 4 (12개월): v1 sunset 발표 (3개월 후)
|
|
Phase 5 (15개월): v1 종료, redirect to v2 또는 410 Gone
|
|
```
|
|
|
|
### Field deprecation (소규모)
|
|
```ts
|
|
// 응답 안 deprecation 메타
|
|
{
|
|
id: '...',
|
|
status: 'paid',
|
|
oldField: 'foo',
|
|
_deprecated: ['oldField'] // 메타
|
|
}
|
|
```
|
|
|
|
또는 docs / changelog 만.
|
|
|
|
### Mobile app (가장 어려움)
|
|
```
|
|
OS app store review = 며칠
|
|
Force update 가능? 사용자 화남.
|
|
→ Backwards-compatible long term + min app version
|
|
```
|
|
|
|
```ts
|
|
// 사용자가 너무 옛 app
|
|
if (req.headers['x-app-version'] < '2.0') {
|
|
return res.status(426).json({ // Upgrade Required
|
|
type: '...',
|
|
title: 'Please update the app',
|
|
minVersion: '2.0',
|
|
});
|
|
}
|
|
```
|
|
|
|
### Internal API — 다름
|
|
- 같은 organization = breaking 가능 + monorepo 동시 update.
|
|
- 다른 팀 = public API 처럼.
|
|
|
|
### Federation / GraphQL
|
|
```graphql
|
|
type Order {
|
|
id: ID!
|
|
status: OrderStatus!
|
|
oldField: String @deprecated(reason: "Use newField")
|
|
newField: String
|
|
}
|
|
```
|
|
|
|
→ Breaking 없이 점진.
|
|
|
|
### Schema breaking 검출 (CI)
|
|
```bash
|
|
# OpenAPI
|
|
oasdiff diff old.yaml new.yaml --breaking-only
|
|
|
|
# GraphQL
|
|
graphql-inspector diff schema.old.graphql schema.new.graphql
|
|
```
|
|
|
|
→ PR check.
|
|
|
|
## 🤔 의사결정 기준
|
|
| 상황 | 전략 |
|
|
|---|---|
|
|
| Public REST | URL versioning (/v1, /v2) |
|
|
| Stripe-like 정밀 | Date versioning |
|
|
| Internal | Header / monorepo lockstep |
|
|
| GraphQL | @deprecated + 새 field |
|
|
| Mobile-heavy | Backwards-compatible 길게 |
|
|
| 작은 / fast iteration | Single version + 동시 release |
|
|
|
|
## ❌ 안티패턴
|
|
- **Breaking 발표 없이**: client 깨짐. communication 필수.
|
|
- **Deprecation 없이 즉시 제거**: 큰 incident.
|
|
- **Version 너무 많음 (v1, v2, v3, v4)**: 유지 부담. 최대 2.
|
|
- **Old version 영원 유지**: 코드 부담. sunset.
|
|
- **Header versioning + cache 안 함**: stale 또는 noise.
|
|
- **Mobile app 강제 update**: 사용자 잃음. min version + 부드러운 prompt.
|
|
- **Schema diff 없는 prod release**: breaking 사고.
|
|
|
|
## 🤖 LLM 활용 힌트
|
|
- URL versioning + Sunset header + 6+ 개월.
|
|
- CI 에서 schema diff 검사.
|
|
- Backwards-compatible 우선, breaking = new major.
|
|
|
|
## 🔗 관련 문서
|
|
- [[API_REST_Best_Practices]]
|
|
- [[API_OpenAPI_Spec]]
|
|
- [[Backend_Feature_Flags_Deep]]
|