REST API 설계 원칙
REST(Representational State Transfer)는 HTTP를 최대한 활용하는 API 아키텍처 스타일이다. “REST하다”는 표현은 종종 남용되지만, 핵심 원칙은 명확하다. AI 모델을 서비스할 때도 이 원칙을 따르면 클라이언트 코드가 예측 가능해지고 유지보수가 쉬워진다.
리소스를 명사로 표현하라
섹션 제목: “리소스를 명사로 표현하라”가장 흔한 실수는 URL에 동사를 쓰는 것이다.
# 잘못된 예: 동사 중심GET /getUser/123POST /createItemDELETE /deleteModel/gpt-4
# 올바른 예: 명사(리소스) 중심GET /users/123POST /itemsDELETE /models/gpt-4리소스는 컬렉션(/users)과 단일 항목(/users/123)으로 구분한다. 중첩 관계는 경로로 표현한다.
GET /users/123/predictions # 유저 123의 예측 목록POST /users/123/predictions # 유저 123의 예측 생성GET /users/123/predictions/456 # 유저 123의 예측 456 조회HTTP 메서드 시맨틱스
섹션 제목: “HTTP 메서드 시맨틱스”| 메서드 | 의미 | 멱등성 | 안전성 | 사용 예 |
|---|---|---|---|---|
| GET | 조회 | O | O | 리소스 읽기 |
| POST | 생성/액션 | X | X | 새 리소스 생성 |
| PUT | 전체 교체 | O | X | 리소스 전체 업데이트 |
| PATCH | 부분 수정 | X | X | 필드 일부 변경 |
| DELETE | 삭제 | O | X | 리소스 제거 |
멱등성(Idempotency) 이란 같은 요청을 여러 번 보내도 결과가 동일한 성질이다. GET, PUT, DELETE는 멱등이다. POST는 매번 새 리소스를 만들기 때문에 멱등이 아니다. AI 모델 추론 엔드포인트는 보통 POST를 쓴다 — 같은 입력이라도 비결정적 출력이 나올 수 있기 때문이다.
# 예측 요청 (POST)POST /v1/predictions HTTP/1.1Content-Type: application/json
{ "model": "sentiment-v2", "text": "FastAPI is great"}# 응답HTTP/1.1 201 CreatedContent-Type: application/json
{ "id": "pred_abc123", "label": "positive", "score": 0.97, "model": "sentiment-v2"}상태 코드를 의미에 맞게 사용하라
섹션 제목: “상태 코드를 의미에 맞게 사용하라”200 OK — 성공적 조회/수정201 Created — 리소스 생성 완료204 No Content — 성공했지만 반환할 본문 없음 (DELETE)400 Bad Request — 클라이언트 요청 오류 (유효성 검사 실패)401 Unauthorized — 인증 필요403 Forbidden — 인증은 됐지만 권한 없음404 Not Found — 리소스 없음422 Unprocessable Entity — 형식은 맞지만 의미상 오류 (FastAPI 기본값)500 Internal Server Error — 서버 오류FastAPI는 Pydantic 유효성 검사 실패 시 자동으로 422를 반환한다. 직접 오류를 낼 때는 HTTPException을 사용한다.
from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.get("/models/{model_id}")async def get_model(model_id: str): model = db.get(model_id) if model is None: raise HTTPException(status_code=404, detail="모델을 찾을 수 없습니다") return model버전 관리
섹션 제목: “버전 관리”API는 변한다. 버전을 URL에 명시하면 하위 호환성을 유지할 수 있다.
# URL 경로 버전 (가장 흔함)/v1/predictions/v2/predictions
# 헤더 버전 (깔끔하지만 캐시하기 어려움)Accept: application/vnd.myapi.v2+jsonFastAPI에서는 APIRouter의 prefix로 버전을 분리한다.
from fastapi import APIRouter
v1_router = APIRouter(prefix="/v1")v2_router = APIRouter(prefix="/v2")
@v1_router.post("/predictions")async def predict_v1(text: str): ...
@v2_router.post("/predictions")async def predict_v2(payload: PredictRequest): ...페이지네이션과 필터링
섹션 제목: “페이지네이션과 필터링”대량의 데이터를 한 번에 반환하면 안 된다. 커서 기반과 오프셋 기반 두 가지 방식이 있다.
# 오프셋 기반 (구현 쉬움, 대용량에 부적합)GET /predictions?limit=20&offset=40
# 커서 기반 (실시간 데이터에 적합)GET /predictions?limit=20&cursor=pred_abc123필터링은 쿼리 파라미터로 표현한다.
from fastapi import FastAPI, Queryfrom typing import Optional
@app.get("/predictions")async def list_predictions( limit: int = Query(20, le=100), offset: int = Query(0, ge=0), model: Optional[str] = None, label: Optional[str] = None,): query = db.query(Prediction) if model: query = query.filter(Prediction.model == model) if label: query = query.filter(Prediction.label == label) return query.offset(offset).limit(limit).all()API 설계 체크리스트
섹션 제목: “API 설계 체크리스트”| 항목 | 확인 |
|---|---|
| URL에 동사가 없는가? | ☐ |
| HTTP 메서드가 의미에 맞는가? | ☐ |
| 상태 코드가 정확한가? | ☐ |
| API 버전이 명시되어 있는가? | ☐ |
| 컬렉션 엔드포인트에 페이지네이션이 있는가? | ☐ |
오류 응답에 detail 필드가 있는가? | ☐ |
| 인증이 필요한 엔드포인트가 명확한가? | ☐ |
| 큰 페이로드(이미지, 바이너리)는 별도 업로드 흐름을 쓰는가? | ☐ |
핵심 정리
섹션 제목: “핵심 정리”- 리소스는 명사 — URL에 동사를 쓰지 않는다
- HTTP 메서드가 동사 역할 — GET/POST/PUT/PATCH/DELETE를 올바르게 쓴다
- 멱등성 인식 — GET, PUT, DELETE는 멱등, POST는 아니다
- 버전은 URL에 —
/v1/접두사가 가장 실용적이다 - 페이지네이션 필수 — 컬렉션 엔드포인트는 항상 limit/offset 또는 커서를 지원한다