본문으로 건너뛰기
김신건의 로그

[API Design] REST API: 원칙, 자원 모델링, HATEOAS, 버전 관리

· 수정 · 📖 약 3분 · 1,042자/단어 #rest #api-design #backend #http
REST API Design, REST, RESTful API, Resource modeling, HATEOAS, Richardson Maturity Model, API versioning

정의

REST (Representational State Transfer) 는 Roy Fielding 의 2000년 박사논문에서 정립된 분산 시스템 아키텍처 스타일. HTTP 의 자원 / 표현 / 상태 전이 를 자연스럽게 활용.

IMPORTANT

“REST” 라는 단어는 대부분 실무에서 HTTP+JSON CRUD 를 의미. Fielding 의 순수 REST (HATEOAS 포함) 는 드물게 적용. 이 페이지는 실무 REST진짜 REST 둘 다 정리.

Richardson Maturity Model (REST 의 4 단계)

flowchart TB
    L0["Level 0: 단일 endpoint<br/>POST /api 로 모든 작업"] --> L1["Level 1: 자원 분리<br/>GET /users, /orders"]
    L1 --> L2["Level 2: HTTP method + status<br/>GET/POST/PUT/DELETE"]
    L2 --> L3["Level 3: HATEOAS<br/>응답에 next action 링크"]

실무 대부분은 Level 2 에 머문다. Level 3 은 Stripe, GitHub API 의 일부 정도.

자원 모델링

좋은 패턴나쁜 패턴
GET /users/42GET /getUser?id=42
POST /users (생성)POST /createUser
DELETE /users/42POST /deleteUser
GET /users/42/ordersGET /userOrders?uid=42
POST /users/42/email/verify (action)GET /verifyUserEmail?uid=42

규칙:

  1. 명사로 자원 표현 (users, orders)
  2. 복수형 권장 (users not user)
  3. 상태 변경 은 HTTP method 로
  4. 체이닝 자원으로 관계 표현 (/users/42/orders/9)
  5. 비-CRUD action복합 명사 (/orders/9/cancel, /payments/p_123/refund)

HTTP Method 매핑

Method멱등안전사용
GETOO조회
HEADOO헤더만
POSTXX생성, 비-멱등 action
PUTOX전체 교체
PATCHX (대개)X부분 갱신
DELETEOX삭제

PUT vs PATCH 상세

PUT: 전체 교체. 보내지 않은 필드는 null / 기본값으로 덮어씀.

PUT /users/42
Content-Type: application/json

{ "name": "koa", "email": "koa@example.com", "phone": "010-1234-5678" }
# 전체 객체를 보내야 함. phone 빠뜨리면 서버에서 phone=null 로 덮어씀

PATCH: 부분 갱신. 보낸 필드만 변경.

PATCH /users/42
Content-Type: application/json

{ "email": "newemail@example.com" }
# email 만 변경. name, phone 은 그대로

JSON Patch (RFC 6902): PATCH 의 표준 형식:

PATCH /users/42
Content-Type: application/json-patch+json

[
  { "op": "replace", "path": "/email", "value": "newemail@example.com" },
  { "op": "add", "path": "/tags/-", "value": "premium" },
  { "op": "remove", "path": "/phone" }
]
op의미
add필드 추가 또는 배열 append
remove필드 제거
replace필드 값 교체
move필드 이동
copy필드 복사
test값 검증 (실패 시 전체 취소)

Status Code 패턴

코드사용
200성공 (GET, PUT, PATCH)
201생성 (POST). Location 헤더로 새 자원 URL
202비동기 접수
204본문 없음 성공 (DELETE)
400입력 형식 / 필수 누락
401인증 필요
403인증 됐지만 권한 없음
404자원 없음
409충돌 (중복, 동시성)
422형식은 OK, 의미 검증 실패
429rate limit 초과

에러 응답 (RFC 7807)

{
  "type": "https://example.com/probs/validation",
  "title": "Validation failed",
  "status": 422,
  "detail": "Email is required",
  "instance": "/requests/12345",
  "errors": [
    { "field": "email", "code": "REQUIRED" },
    { "field": "phone", "code": "INVALID_FORMAT" }
  ]
}
필드의미
type문제 타입 URI (안정적인 문서 링크)
title사람이 읽는 요약 (고정 메시지)
statusHTTP status code
detail이 요청의 구체적 설명
instance이 오류의 고유 URI

Idempotency-Key 패턴

네트워크 재시도 시 중복 생성 방지:

sequenceDiagram
    participant C as Client
    participant S as Server
    participant DB as Database

    C->>S: POST /payments (Idempotency-Key: abc-123)
    S->>DB: 키 abc-123 존재 확인
    DB-->>S: 없음
    S->>DB: 결제 처리 + 키 abc-123 저장
    S-->>C: 201 Created

    Note over C,S: 네트워크 오류 후 재시도
    C->>S: POST /payments (Idempotency-Key: abc-123)
    S->>DB: 키 abc-123 존재 확인
    DB-->>S: 있음 (기존 결과 반환)
    S-->>C: 200 OK (동일 결과, 재처리 없음)
POST /payments
Idempotency-Key: abc-123-xyz
Content-Type: application/json

{ "amount": 10000, "currency": "KRW", "method": "card" }

Rate Limiting 응답 헤더

HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1720123456
Retry-After: 3600
Content-Type: application/problem+json

{
  "type": "https://api.example.com/probs/rate-limited",
  "title": "Too Many Requests",
  "status": 429,
  "detail": "Rate limit exceeded. Retry after 3600 seconds."
}
헤더의미
X-RateLimit-Limit윈도우당 허용 요청 수
X-RateLimit-Remaining현재 윈도우 남은 요청 수
X-RateLimit-Reset윈도우 초기화 시각 (Unix epoch)
Retry-After다시 시도까지 대기 초

필터링 / 정렬 / 검색 패턴

# 필터링
GET /orders?status=pending&customer_id=42

# 정렬 (- prefix = 내림차순)
GET /orders?sort=-created_at,amount

# 필드 선택 (sparse fieldsets)
GET /users?fields=id,name,email

# 전문 검색
GET /products?q=mechanical+keyboard

# 페이지네이션 (cursor 기반)
GET /orders?cursor=eyJpZCI6MTAwfQ&limit=20

페이지네이션

flowchart LR
    A["Offset/Limit<br/>?offset=20&limit=10"] --> B["페이지 N 빠른 점프 가능<br/>단, 큰 offset 시 O(N)"]
    C["Cursor<br/>?cursor=abc123"] --> D["연속 스트림, 정확<br/>단, 임의 점프 불가"]
    E["Page/Size<br/>?page=3&size=10"] --> F[옛 패턴]

TIP

대량 데이터 + 무한 스크롤 = 거의 항상 cursor 기반. Stripe, GitHub, Twitter API 모두.

응답 포맷 (envelope)

{
  "data": [...],
  "meta": {
    "total": 1024,
    "page": 1,
    "per_page": 20
  },
  "links": {
    "self": "...",
    "next": "...",
    "prev": null
  }
}

CAUTION

envelope 의 너무 깊은 nesting클라이언트 코드 복잡. flat + 표준 (RFC 7807 problem details for errors) 권장.

HATEOAS

응답에 다음 가능 action 의 링크 포함:

{
  "id": "o_123",
  "status": "pending",
  "links": {
    "self": "/orders/o_123",
    "cancel": "/orders/o_123/cancel",
    "pay": "/orders/o_123/pay"
  }
}

NOTE

클라이언트가 URL 을 미리 알 필요 없이 응답의 링크만 따라 가는 진짜 REST. 실무에서는 드물지만 GitHub API 가 좋은 예.

API 버전 관리

flowchart LR
    Q{버전 표시 방법}
    Q --> URL["URL: /v1/users<br/>/v2/users"]
    Q --> Header["헤더: Accept: application/vnd.api+json;v=2"]
    Q --> Query["Query: /users?api-version=2"]
방식장점단점
URL (/v1/)명시적, 캐싱 친화URL 영구 변경
Accept 헤더URL 깔끔디버깅 / 캐시 어려움
Query단순URL 변형

Stripe날짜 기반 버전 (Stripe-Version: 2023-10-16). 가장 세밀하고 안전한 호환성.

보안 고려사항

flowchart TD
    Auth{"인증 방식"}
    Auth -->|내부 서비스| APIKey["API Key<br/>(간단, rotation 필요)"]
    Auth -->|사용자 대리| JWT["JWT / OAuth2<br/>(scope 기반 권한)"]
    Auth -->|서비스간| mTLS["mTLS<br/>(인증서 기반)"]
    Auth -->|B2B 엔터프라이즈| SAML["SAML / OIDC<br/>(SSO 통합)"]
항목고려사항
HTTPS모든 endpoint TLS 필수. HTTP redirect
Rate LimitingDDoS, brute force 방어
Input ValidationSQL injection, XSS 방어. schema validation
Auth headerAuthorization: Bearer + 짧은 만료
CORSallowed origin 명시. wildcard * 금지 (인증 API)
Audit Log민감 operation 로깅 (who, what, when)

흔한 함정

WARNING

  1. /users/getById 같은 동사 URL = REST 의 자원 사상 위반.
  2. 모든 변경에 PUT = PATCH 가 적절한 부분 갱신에도 PUT 사용 → race condition.
  3. POST비-멱등 위험 = 네트워크 재시도 시 중복 생성. Idempotency-Key 헤더로 해결.
  4. 버전 없이 시작 = breaking change 시 모든 클라이언트 동시 마이그레이션 강요.
  5. 에러 응답 포맷 불일치 = 각 endpoint 가 다른 에러 형식. RFC 7807 통일.
  6. 큰 offset pagination = ?offset=100000 은 DB 에서 O(N) 풀 스캔. cursor 기반으로.

관련 위키

이 글의 용어 (5개)
[API Design] GraphQL: 단일 endpoint, N+1, persisted queriesapi-design
정의 GraphQL (Facebook, 2015) 은 클라이언트가 필요한 필드만 명시 하는 query language + 런타임. 단일 endpoint, typed schema,…
[API Design] OpenAPI / Swagger: 스펙, 코드 생성, contract testingapi-design
정의 OpenAPI Specification (OAS) 은 REST API 를 기술하는 YAML/JSON 형식. 옛 이름 Swagger. 2017 부터 OpenAPI Initia…
[Network] gRPC: HTTP/2 + Protobuf, 4가지 streaming 패턴network
정의 gRPC 는 HTTP/2 위에서 Protobuf 직렬화 로 동작하는 고성능 RPC 프레임워크. Google 내부 Stubby 의 오픈소스 후계. 핵심 4가지: 1. Prot…
[Network] HTTP/1.1: keep-alive, pipelining, chunked transfernetwork
정의 HTTP/1.1 (1997, RFC 9112 으로 재정리) 는 텍스트 기반 stateless request/response 프로토콜. 2026 시점에도 대부분의 트래픽이 H…
[Pattern] Idempotency Keys: 중복 요청 안전 처리distributed-systems
정의 Idempotency = 같은 요청을 N번 보내도 결과가 1번과 동일. 분산 시스템 / 결제 / API 의 안전망. [!IMPORTANT] 네트워크는 항상 timeout /…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기