---
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]]
