REST API 설계은(는) 무엇을 하나요?
API는 제품입니다. 소비자의 인지 모델이 형태를 결정하며, 데이터베이스 스키마가 아닙니다. 이 스킬이 방지하는 고비용 실수는 일관성 없는 표면(혼합된 명명, 잘못된 상태 코드, 증가하는 테이블에 대한 오프셋 페이지네이션)을 출시하는 것이며, 모든 소비자가 이를 하드코딩하여 통합 후에는 사실상 수정 불가능하게 만듭니다.
운영 절차
다음 순서로 작업하세요. 리소스는 메서드보다 먼저 나와야 합니다. 메서드 의미론은 리소스가 무엇인지에 따라 달라지기 때문입니다. 오류 및 페이지네이션 계약은 명세서가 작성되기 전에 수정해야 합니다. 사후에 수정하면 소비자를 망가뜨리기 때문입니다.
1단계: 입력 수집
설계하기 전에 수집하세요. 모든 가정은 추측으로 표시하세요.
- 소비자: 내부 서비스, 퍼스트파티 클라이언트 또는 외부 개발자. 외부 소비자는 일관성 기준을 높입니다. 알 수 없는 경우 기본값은 외부로 설정합니다.
- 도메인의 핵심 명사와 명사 간의 소유 관계(중첩을 결정).
- 예상 컬렉션 크기 및 성장. 약 10,000행을 초과할 수 있는 목록은 오프셋 페이지네이션을 배제합니다.
- 엔드포인트별 읽기/쓰기 비율 및 지연 시간 예상.
- 인증 모델(401 대 403 정확성에 영향을 미침).
2단계: 리소스 명명
- 복수 명사: /users, /orders, /products.
- 소유권을 위해 한 단계 중첩: /users/{id}/orders. 두 수준 이상 깊게 중첩하지 마세요. 대신 필터로 평탄화하세요(?user_id=...이 /users/{id}/orders/{oid}/items/{iid}보다 낫습니다).
- 경로에 동사를 사용하지 마세요. HTTP 메서드가 동사입니다. CRUD 동사에 맞지 않는 실제 동작(예: 송장 발송)의 경우 하위 리소스 POST로 모델링하세요: POST /invoices/{id}/send-attempts.
- 일관된 케이스: 여러 단어 경로 세그먼트는 케밥 케이스(kebab-case), JSON 필드는 스네이크 케이스(snake_case)를 사용하고 한 번 선택하면 모든 곳에 적용하세요.
3단계: 메서드 및 상태 코드 할당
- GET: 읽기, 멱등, 요청 본문 없음. 200과 본문.
- POST: 생성. 201과 새 리소스를 가리키는 Location 헤더를 반환합니다. 클라이언트가 후속 GET을 건너뛰도록 생성된 표현을 본문에 반환합니다.
- PUT: 전체 교체, 멱등. 누락된 필드는 "유지"가 아니라 "지움"을 의미합니다.
- PATCH: 부분 업데이트(JSON Merge Patch 또는 JSON Patch 중 명세서에 명시).
- DELETE: 제거, 멱등, 204 반환. 이미 삭제된 리소스를 삭제하면 204 또는 404를 반환합니다. 하나를 선택하고 문서화하세요.
- 오류: 400(잘못된 입력), 401(누락되거나 유효하지 않은 자격 증명), 403(유효한 자격 증명이지만 권한 없음), 404(존재하지 않는 리소스, 존재 노출이 유출인 경우 403 대신 404 사용), 409(상태 충돌), 422(형식은 올바르지만 의미적으로 유효하지 않은 입력), 429(속도 제한, Retry-After 포함), 500(서버 오류) – 스택 추적이나 내부 식별자를 절대 노출하지 마세요.
4단계: 오류 계약 수정
모든 오류에 대해 하나의 형태, 먼저 기계가 읽을 수 있는 코드:
{
"error": "validation_error",
"message": "이메일은 필수입니다",
"field": "email",
"request_id": "req_abc123"
}
request_id는 서버 측에 기록되어 지원팀이 상관 관계를 추적할 수 있어야 합니다. 사람이 읽는 메시지는 로그를 읽는 개발자를 위한 것이며, 최종 사용자 표시용이 아닙니다.
5단계: 페이지네이션 및 필터링 수정
- 성장하는 모든 컬렉션에는 커서 기반 페이지네이션을 사용하세요: 불투명 커서와 has_more 플래그를 반환합니다. 오프셋 페이지네이션은 작고 제한적이며 관리자 전용 목록에만 허용됩니다. 동시 쓰기 시 선형적으로 성능이 저하되고 행을 건너뛰거나 중복합니다.
- 기본 페이지 크기 25, 최대 100. 최대값을 초과하는 페이지 크기는 400으로 거부하고, 자동으로 조정하지 마세요.
- 필터링 및 정렬은 쿼리 파라미터를 통해: ?status=active&sort=created_at:desc. 필터링 가능한 필드를 문서화하고, 알 수 없는 필터 파라미터는 무시하지 말고 400으로 거부하세요(무시는 클라이언트 버그를 숨깁니다).
6단계: 성능 및 페이로드 예산 적용
- p95 지연 시간 예산: 대화형 엔드포인트는 300ms, 보고서/내보내기는 1초. 작업이 1초를 충족할 수 없으면 비동기로 만드세요: 202 Accepted와 폴링할 상태 리소스.
- 응답 페이로드 예산: 일반 응답은 100KB 미만으로 유지하세요. 1MB를 초과하는 것은 페이지네이션, 필드 선택(?fields=) 또는 전용 내보내기 플로우가 필요합니다.
- 주요 변경 사항은 URL 경로에 버전을 명시하세요(/v1/). 추가 변경 사항(새로운 선택적 필드)은 버전 범프가 필요하지 않습니다. 지원 중단 타임라인 자체는 api-versioning-strategist에 전달하세요.
작업 대비 예시
나쁜 예:
POST /getUserOrders?userId=42&page=3
→ 200 { "data": [...], "error": null }
경로에 동사, POST에 GET 의미론, 오프셋 페이지네이션, 오류 필드를 래핑하는 200으로 클라이언트가 호출 실패 여부를 알기 위해 본문을 파싱해야 합니다.
좋은 예:
GET /users/42/orders?status=shipped&limit=25&cursor=eyJpZCI6OTh9
→ 200 { "orders": [...], "next_cursor": "eyJpZCI6MTIzfQ", "has_more": true }
→ 404 { "error": "not_found", "message": "사용자 42가 존재하지 않습니다", "request_id": "req_9f2" }
명사 경로, 소유권 중첩, 커서 페이지네이션, 상태 코드가 결과를 전달합니다.
산출물
다음을 포함하는 엔드포인트 명세서를 생성하세요: 리소스 테이블(경로, 메서드, 메서드별 상태 코드), 공유 오류 형태, 페이지네이션 계약(커서 형식, 기본값, 최대값), 필터링 허용 목록, 엔드포인트별 지연 시간 및 페이로드 예산.
하지 말아야 할 것
- API에 데이터베이스 스키마를 미러링하지 마세요. 소비자가 변경하고 싶은 내부에 결합됩니다.
- 실패 시 오류 본문과 함께 200을 반환하지 마세요. 클라이언트와 모니터링 모두 상태 코드를 기준으로 합니다.
- 무제한 컬렉션에 오프셋 페이지네이션을 사용하지 마세요. 쓰기 시 행을 중복하고 건너뛰며 선형적으로 성능이 저하됩니다.
- 경로를 두 수준 이상 깊게 중첩하지 마세요. 깊은 중첩은 하나의 액세스 경로를 인코딩하고 다른 모든 경로를 금지합니다.
- 알 수 없는 쿼리 파라미터를 자동으로 무시하지 마세요. 오타가 난 필터가 필터링되지 않은 데이터를 반환하면 데이터 유출 수준의 버그입니다.
품질 기준
- 소비자가 질문 없이 명세서만으로 클라이언트를 구현할 수 있어야 합니다.
- 모든 엔드포인트는 오류 사례를 포함한 상태 코드를 명시합니다.
- 모든 컬렉션 엔드포인트는 하나의 페이지네이션 계약을 공유하고 모든 오류는 하나의 형태를 공유합니다.
- 동일한 개념은 모든 경로와 페이로드에서 동일한 이름을 가집니다.
- 각 엔드포인트는 명시적인 지연 시간 예산이 있으며 1초를 초과하는 것은 비동기입니다.