--- 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 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: ; rel="deprecation" ``` ```ts res.set('Deprecation', 'true'); res.set('Sunset', 'Sat, 31 Dec 2026 23:59:59 GMT'); res.set('Link', '; 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]]