[REST 설계] URI 식별자(Identifier) 설계 가이드
1) 원칙 요약
- 리소스는 명사, 복수형: 동사가 아니라 무엇을 다루는지 드러내기
GET /users
,POST /orders
- 계층 구조는 관계를 표현: 포함·소유 관계만 중첩
GET /users/{userId}/orders/{orderId}
- 조작은 HTTP 메서드에 맡기기: URI에 동사 넣지 않기
❌POST /createUser
→ ✅POST /users
- 일관성 유지: 전 서비스 공통 네이밍·구분자·규칙 강제
- 불변·불투명 ID 선호: 내부 키 노출 지양, UUID/ULID 등 사용
- 클라이언트 제어용 파라미터는 쿼리스트링: 필터링/정렬/페이지네이션
- 표현/포맷은 헤더 우선:
Accept
,Content-Type
로 컨트롤
2) 네이밍 컨벤션
- 케밥 케이스(kebab-case):
purchase-orders
,reset-tokens
- 복수형 컬렉션:
users
,products
,invoices
- 하위 리소스는 포함 관계일 때만
- 포함 관계:
GET /users/{id}/addresses
- 단순 연결(느슨한 관계)이라면 상위 컬렉션 + 필터:
GET /orders?userId={id}
- 포함 관계:
- 슬래시 의미: 경로 분리자(계층). 의미 없는 트레일링 슬래시 금지
✅/users/123
❌/users/123/
3) 리소스 식별자(ID) 전략
- 불투명 식별자(권장): UUID/ULID/雪花(Snowflake) 등
- 장점: 노출 안정성, 변경 용이성, 샤딩/병렬 생성 유리
- 자연키(지양): 이메일/전화번호/자동증가 PK 등은 변경 위험·추측 가능성
- 슬러그(slug): 사람이 읽기 쉬운 보조 식별자
- 공개 페이지용으로
GET /posts/{slug}
허용하되 내부 ID 병행 저장 추천
- 공개 페이지용으로
- 혼합 키(복합키): 상위-하위 강한 종속성 있을 때만 경로 계층으로 표현
GET /shops/{shopId}/products/{productId}
4) 컬렉션, 단건, 관계 표현
# 컬렉션
GET /users
POST /users
# 단건
GET /users/{userId}
PATCH /users/{userId}
DELETE /users/{userId}
# 하위 컬렉션(포함 관계)
GET /users/{userId}/orders
POST /users/{userId}/orders
# 특정 하위 리소스
GET /users/{userId}/orders/{orderId}
5) 필터링·정렬·페이지네이션: 쿼리 파라미터
- 필터:
GET /orders?status=PAID&from=2025-08-01&to=2025-08-12
- 정렬:
GET /products?sort=price,-createdAt
(-
는 내림차순) - 페이지네이션
- 오프셋 기반:
page
,pageSize
→ 단순, 대용량 비권장 - 커서 기반(권장):
cursor
,limit
→ 안정적 스크롤 - 예:
GET /events?cursor=eyJpZCI6Ij...&limit=50
- 오프셋 기반:
- 필드 선택(선택적):
GET /users?fields=id,name,avatarUrl
규칙: 리소스의 “무엇”은 경로, “어떻게 볼지”는 쿼리.
6) 동작(Action) 표현이 필요한 경우
REST에선 동작을 HTTP 메서드로 표현하지만, 도메인 고유 행위가 필요할 땐 서브리소스 형태의 컨트롤 엔드포인트를 사용합니다.
POST /orders/{orderId}:cancel
POST /carts/{cartId}:checkout
POST /auth:reset-password
- 콜론(
:action
) 혹은/_actions/{action}
등 팀 표준을 하나로 통일 - 멱등성 주의: 취소처럼 멱등적이면
PUT /orders/{id}:cancel
고려
7) 버전 관리
- 권장: URI 버전은 명확하고 캐싱·문서화 쉬움
GET /v1/users
→ 추후GET /v2/users
- 대안: 헤더 버전(커스텀
Accept
미디어 타입)Accept: application/vnd.example.user+json;version=2
- 가이드
- 브레이킹 체인지만 버전업
- 실험 기능은 프리픽스(
/beta
) 또는 플래그로 분리
8) 국제화·다국어
- URI는 언어 불변(영문 리소스명 고정).
- 표현 언어는 헤더로:
Accept-Language: ko-KR
- 다국어 슬러그가 필요하면 쿼리나 별도 필드로 관리.
9) 보안·민감정보
- 민감정보는 경로에 넣지 않기: 토큰·이메일·전화번호
- ✅ 헤더/바디로 전달
- 추측 가능 경로 금지: 순차 ID, 의미 과다 노출 피하기
- 리소스 접근 제어는 별개: URI는 공개되어도 권한 검증 필수
10) 에러와 일관 응답
- 잘못된 경로/식별자:
404 Not Found
- 유효하지 않은 쿼리 파라미터:
400 Bad Request
- 표준화된 에러 바디 예:
{ "error": "INVALID_QUERY", "message": "Unknown sort field: created_time", "details": { "allowedSorts": ["createdAt", "price"] }, "traceId": "b6c1..." }
11) 좋은/나쁜 예시 모음
좋은 예
GET /users/8c1f.../orders?status=PAID&sort=-createdAt&limit=20
POST /v1/posts
/GET /v1/posts/{postId}
POST /auth:reset-password
나쁜 예
POST /createUser
(동사 사용)/userList
(복수형 규칙 위반 + 컬렉션 의미 불명확)/orders/cancel?id=123
(행위를 쿼리로)/users/123/permissions/456/roles/789/groups/1011
(과도한 중첩)
12) 팀 체크리스트
- 복수형 컬렉션/명사형 리소스 사용
- 케밥 케이스 적용, 트레일링 슬래시 금지
- 포함 관계만 중첩, 나머지는 쿼리 필터
- 액션은
:action
(혹은 팀 표준)으로 최소화 - 필터/정렬/페이지네이션 쿼리 규격 통일
- 불투명 ID 사용(가능하면 커서 기반 페이지네이션)
- 버전 정책 합의(v1 기준, 브레이킹 시에만 증가)
- 민감정보 경로 금지, 권한검증 분리
- 에러 스키마와 코드 매핑 표준화
13) 샘플 API 스펙 조각 (OpenAPI 느낌)
GET /v1/orders:
summary: List orders
parameters:
- in: query
name: status
schema: { type: string, enum: [PENDING, PAID, CANCELLED] }
- in: query
name: sort
schema: { type: string, example: "price,-createdAt" }
- in: query
name: cursor
schema: { type: string }
- in: query
name: limit
schema: { type: integer, default: 50, maximum: 200 }
responses:
"200":
description: OK
(대표 이미지는 GPT를 활용하여 생성한 이미지를 사용해 보았습니다.)