URL 경로, 헤더, 쿼리 파라미터 버전 관리 전략과 AI 서비스에서의 모델 버전 분리, 프롬프트 버전 관리, 폐기 정책을 학습합니다.
API는 한 번 공개되면 외부 클라이언트가 의존합니다. 기능 개선이나 설계 변경이 기존 클라이언트를 깨트리면 안 됩니다. 특히 AI 서비스는 모델 업데이트가 빈번하고, 모델 변경이 API 응답의 구조와 품질에 직접적인 영향을 미치므로 버전 관리가 더욱 중요합니다.
가장 직관적이고 널리 사용되는 방식입니다. OpenAI, Anthropic, Google AI 등 주요 AI 서비스 제공자가 이 방식을 채택합니다.
# 버전 1
POST https://api.example.com/v1/chat/completions
POST https://api.example.com/v1/embeddings
# 버전 2
POST https://api.example.com/v2/chat/completions
POST https://api.example.com/v2/embeddingsfrom fastapi import FastAPI, APIRouter
app = FastAPI()
# 버전별 라우터
v1_router = APIRouter(prefix="/v1", tags=["v1"])
v2_router = APIRouter(prefix="/v2", tags=["v2"])
@v1_router.post("/chat/completions")
async def v1_chat_completion(request: V1CompletionRequest):
"""v1: 기본 텍스트 완성"""
return await process_v1(request)
@v2_router.post("/chat/completions")
async def v2_chat_completion(request: V2CompletionRequest):
"""v2: 멀티모달 지원, 구조화된 출력 추가"""
return await process_v2(request)
app.include_router(v1_router)
app.include_router(v2_router)장점: 명확하고 직관적, 라우팅이 간단, 캐싱 친화적 단점: URL이 버전으로 오염, 리소스 중복 가능성
HTTP 헤더로 버전을 지정합니다. URL을 깨끗하게 유지할 수 있지만, 테스트와 디버깅이 번거롭습니다.
# 커스텀 헤더
POST https://api.example.com/chat/completions
X-API-Version: 2026-03-01
# Accept 헤더
POST https://api.example.com/chat/completions
Accept: application/vnd.example.v2+jsonfrom fastapi import Header, HTTPException
@app.post("/chat/completions")
async def chat_completion(
request: CompletionRequest,
x_api_version: str = Header(
default="2026-01-01",
alias="X-API-Version",
),
):
if x_api_version >= "2026-03-01":
return await process_v2(request)
else:
return await process_v1(request)장점: 깨끗한 URL, 세밀한 버전 제어 단점: 브라우저에서 테스트 어려움, 문서화 복잡
Stripe가 도입하고 Anthropic이 채택한 방식으로, API 버전을 날짜로 표현합니다.
# 헤더로 API 버전(날짜) 지정
POST https://api.example.com/v1/chat/completions
anthropic-version: 2026-03-01
# 기본값: 계정 생성 시점의 최신 버전
# 최신 버전으로 마이그레이션은 클라이언트 선택from datetime import date
# 버전별 변경 사항 정의
VERSION_CHANGES = {
"2026-01-01": {
"description": "초기 버전",
"changes": [],
},
"2026-02-15": {
"description": "스트리밍 사용량 정보 추가",
"changes": [
"스트리밍 응답 마지막 청크에 usage 필드 포함",
"finish_reason에 'content_filter' 추가",
],
},
"2026-03-01": {
"description": "멀티모달 입력 지원",
"changes": [
"messages.content가 string 또는 ContentPart 배열",
"tool_choice 필드 추가",
"response_format에 json_schema 타입 추가",
],
},
}
def get_effective_version(requested: str) -> str:
"""요청된 날짜 이하의 가장 최신 버전을 반환"""
versions = sorted(VERSION_CHANGES.keys())
effective = versions[0]
for v in versions:
if v <= requested:
effective = v
return effective날짜 기반 버전 관리는 빈번한 소규모 변경이 있는 AI API에 특히 적합합니다. 주요 버전 번호(v1, v2)의 대규모 전환 없이, 개별 변경 사항을 날짜로 추적할 수 있습니다. Anthropic의 API가 이 방식을 채택한 이유입니다.
| 전략 | 채택 예시 | 적합한 상황 |
|---|---|---|
| URL 경로 | OpenAI, Google AI | 명확한 메이저 버전 구분, 공개 API |
| 헤더 기반 | GitHub, Azure | RESTful 순수주의, 세밀한 제어 |
| 날짜 기반 | Stripe, Anthropic | 빈번한 소규모 변경, 점진적 마이그레이션 |
기존 클라이언트를 깨트리지 않는 변경입니다. 새 버전 없이 적용할 수 있습니다.
# 1. 새 선택적 필드 추가
class CompletionRequestV1(BaseModel):
model: str
messages: list[Message]
temperature: float = 1.0
max_tokens: int | None = None
# 선택적 필드 추가 — 호환
class CompletionRequestV1_1(BaseModel):
model: str
messages: list[Message]
temperature: float = 1.0
max_tokens: int | None = None
top_p: float | None = None # 새 선택적 필드
seed: int | None = None # 새 선택적 필드
# 2. 응답에 새 필드 추가
# 기존: {"id": "...", "choices": [...]}
# 변경: {"id": "...", "choices": [...], "system_fingerprint": "..."}
# 3. 새 엔드포인트 추가
# 기존 엔드포인트에 영향 없이 새 기능 추가
# 4. 새 열거형 값 추가
# finish_reason: "stop" | "length" → "stop" | "length" | "tool_calls"기존 클라이언트를 깨트릴 수 있어 새 API 버전이 필요합니다.
# 1. 필드 제거
# v1: {"id": "...", "object": "...", "model": "..."}
# v2: {"id": "...", "model": "..."} # object 필드 제거
# 2. 필드 타입 변경
# v1: "content": "텍스트 문자열"
# v2: "content": [{"type": "text", "text": "..."}] # string -> array
# 3. 필수 필드 추가
# v1: model, messages만 필수
# v2: model, messages, max_tokens 필수 # 새 필수 필드
# 4. 열거형 값 제거
# v1: model: "gpt-4" | "gpt-4-turbo" | "gpt-3.5-turbo"
# v2: model: "gpt-4o" | "gpt-4o-mini" # 기존 값 제거
# 5. URL 구조 변경
# v1: POST /v1/completions
# v2: POST /v2/chat/completions # 경로 변경
# 6. 기본값 변경
# v1: temperature 기본값 1.0
# v2: temperature 기본값 0.7 # 동작 변경열거형에 새 값을 추가하는 것은 "호환 변경"으로 분류되지만, 클라이언트가 알 수 없는 값에 대해 명시적으로 처리하지 않으면 문제가 됩니다. 따라서 API 문서에 "새로운 열거형 값이 추가될 수 있으니, 알 수 없는 값에 대한 처리를 구현하세요"라고 명시하는 것이 좋습니다.
AI 서비스에서는 "API 버전"과 "모델 버전"이 독립적으로 변화합니다. 이 두 가지를 명확히 분리해야 합니다.
# 모델 별칭과 고정 버전
MODEL_ALIASES = {
# 별칭 → 특정 날짜의 스냅샷으로 매핑
"claude-4": "claude-4-20260301", # 최신 안정 버전
"claude-4-latest": "claude-4-20260301", # 항상 최신
# 고정 버전 (날짜 기반 스냅샷)
"claude-4-20260101": "claude-4-20260101",
"claude-4-20260215": "claude-4-20260215",
"claude-4-20260301": "claude-4-20260301",
}
# 모델 폐기 스케줄
MODEL_DEPRECATION = {
"claude-4-20260101": {
"deprecated_at": "2026-03-01",
"shutdown_at": "2026-06-01",
"replacement": "claude-4-20260301",
},
}
@app.post("/v1/chat/completions")
async def chat_completion(request: CompletionRequest):
resolved_model = MODEL_ALIASES.get(request.model, request.model)
# 폐기 경고
deprecation = MODEL_DEPRECATION.get(resolved_model)
headers = {}
if deprecation:
headers["X-Model-Deprecated"] = deprecation["deprecated_at"]
headers["X-Model-Shutdown"] = deprecation["shutdown_at"]
headers["X-Model-Replacement"] = deprecation["replacement"]
response = await inference(resolved_model, request)
return JSONResponse(
content=response,
headers=headers,
)AI 서비스에서 시스템 프롬프트는 동작을 정의하는 핵심 설정입니다. 프롬프트 변경은 모델 변경만큼이나 출력에 영향을 미치므로 체계적인 버전 관리가 필요합니다.
from dataclasses import dataclass
from datetime import datetime
@dataclass
class PromptVersion:
version: str
content: str
created_at: datetime
author: str
description: str
metrics: dict | None = None # 평가 지표
class PromptRegistry:
"""프롬프트 버전 레지스트리"""
def __init__(self, store):
self.store = store
async def register(
self,
name: str,
content: str,
author: str,
description: str,
) -> PromptVersion:
"""새 프롬프트 버전을 등록합니다."""
existing = await self.store.list_versions(name)
version = f"v{len(existing) + 1}"
prompt = PromptVersion(
version=version,
content=content,
created_at=datetime.now(),
author=author,
description=description,
)
await self.store.save(name, prompt)
return prompt
async def get_active(self, name: str) -> PromptVersion:
"""현재 활성 버전을 반환합니다."""
return await self.store.get_active(name)
async def promote(self, name: str, version: str) -> None:
"""특정 버전을 활성 버전으로 승격합니다."""
prompt = await self.store.get(name, version)
# 평가 지표 확인
if not prompt.metrics:
raise ValueError(
"평가되지 않은 프롬프트는 승격할 수 없습니다"
)
if prompt.metrics.get("accuracy", 0) < 0.85:
raise ValueError(
f"정확도가 기준(85%) 미만입니다: "
f"{prompt.metrics['accuracy']:.1%}"
)
await self.store.set_active(name, version)
async def rollback(self, name: str) -> PromptVersion:
"""이전 활성 버전으로 롤백합니다."""
history = await self.store.get_activation_history(name)
if len(history) < 2:
raise ValueError("롤백할 이전 버전이 없습니다")
previous = history[-2]
await self.store.set_active(name, previous.version)
return previousAPI 버전이나 기능의 폐기는 클라이언트에게 충분한 시간과 정보를 제공해야 합니다.
from fastapi import Request, Response
from fastapi.middleware import Middleware
class DeprecationMiddleware:
"""폐기 예정 API에 경고 헤더를 추가합니다."""
DEPRECATED_ENDPOINTS = {
"/v1/completions": {
"deprecated_at": "2026-01-15",
"sunset_at": "2026-07-15",
"replacement": "/v1/chat/completions",
"migration_guide": "https://docs.example.com/migrate/completions",
},
}
async def __call__(self, request: Request, call_next):
response = await call_next(request)
path = request.url.path
deprecation = self.DEPRECATED_ENDPOINTS.get(path)
if deprecation:
# RFC 8594 Sunset 헤더
response.headers["Sunset"] = deprecation["sunset_at"]
response.headers["Deprecation"] = deprecation["deprecated_at"]
response.headers["Link"] = (
f'<{deprecation["migration_guide"]}>; '
f'rel="deprecation"; '
f'type="text/html"'
)
# 대체 엔드포인트 안내
response.headers["X-Deprecated-Replacement"] = (
deprecation["replacement"]
)
return response@app.get("/v1/migrations")
async def list_migrations() -> list[Migration]:
"""활성 마이그레이션 목록을 반환합니다."""
return [
Migration(
from_version="v1/completions",
to_version="v1/chat/completions",
deprecated_at="2026-01-15",
sunset_at="2026-07-15",
guide_url="https://docs.example.com/migrate/completions",
breaking_changes=[
"요청 형식이 prompt(string)에서 messages(array)로 변경",
"응답의 choices[].text가 choices[].message.content로 변경",
"logprobs 파라미터 구조 변경",
],
code_examples={
"before": 'POST /v1/completions\n{"model":"gpt-4","prompt":"Hello"}',
"after": 'POST /v1/chat/completions\n{"model":"gpt-4","messages":[{"role":"user","content":"Hello"}]}',
},
),
]폐기 예고에서 실제 종료까지 최소 6개월의 유예 기간을 두는 것이 업계 관행입니다. OpenAI는 모델 폐기 시 최소 3개월, API 버전 폐기 시 최소 6개월의 유예 기간을 제공합니다. 이 기간에 사용량 대시보드에서 폐기 예정 API 호출 통계를 제공하면 마이그레이션을 촉진할 수 있습니다.
이 장에서는 API 버전 관리의 세 가지 전략(URL 경로, 헤더, 날짜 기반)을 비교하고, AI 서비스에서 특히 중요한 모델 버전과 API 버전의 분리, 프롬프트 버전 관리, 그리고 체계적인 폐기 전략을 살펴보았습니다.
버전 관리의 핵심은 "기존 클라이언트를 깨트리지 않으면서 진화하는 것"입니다. 호환 변경과 비호환 변경을 정확히 구분하고, 비호환 변경에는 충분한 유예 기간과 마이그레이션 도구를 제공하는 것이 신뢰를 유지하는 방법입니다.
8장에서는 AI API의 비용 제어를 위한 레이트 리미팅을 다룹니다. 전통적인 RPS 기반에서 토큰 기반 레이트 리미팅으로의 전환, 토큰 버킷과 슬라이딩 윈도우 알고리즘, 사용자별/조직별 한도, 그리고 Redis 기반 구현을 살펴봅니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
토큰 기반 레이트 리미팅, 토큰 버킷과 슬라이딩 윈도우 알고리즘, 사용자별 한도 설정, 비용 캡, Redis 기반 구현을 학습합니다.
SSE 기반 토큰 스트리밍 프로토콜, OpenAI 호환 스트리밍 형식, 에러 처리, 클라이언트 취소, 프론트엔드 통합 패턴을 학습합니다.
OpenAPI 스펙에서 타입 안전 SDK를 자동 생성하고, API 문서화, 인터랙티브 플레이그라운드로 개발자 경험을 최적화하는 방법을 학습합니다.