RESTful API 설계 가이드 - 직관적이고 일관성 있는 API 만들기
API를 처음 보는 개발자가 문서 없이도 80% 이해할 수 있어야 합니다.
좋은 규칙을 일관되게 따르는 것이, 완벽한 규칙을 불규칙하게 따르는 것보다 낫습니다.
API는 개발자를 위한 제품입니다. 사용하기 쉬워야 합니다.
# 명사 사용 (동사 X)
GET /users # ✅
GET /getUsers # ❌
# 복수형 사용
GET /users # ✅
GET /user # ❌
# 계층 구조 표현
GET /users/123/orders # ✅
GET /orders?userId=123 # 😐 (대안으로 허용)
# 케밥 케이스 사용
GET /user-profiles # ✅
GET /userProfiles # ❌
GET /user_profiles # ❌
# 필터는 쿼리 파라미터로
GET /products?category=electronics&sort=price # ✅
GET /products/electronics/sortByPrice # ❌
# URL에 동사 포함
POST /users/create # ❌
POST /users # ✅
# URL에 동작 표현
GET /users/search # ❌
GET /users?q=keyword # ✅
# 파일 확장자 포함
GET /users.json # ❌
GET /users # ✅ (Accept 헤더 사용)
# Create - POST
POST /users # 새 사용자 생성
Body: { "name": "홍길동", "email": "hong@example.com" }
# Read - GET
GET /users # 목록 조회
GET /users/123 # 단건 조회
# Update - PUT/PATCH
PUT /users/123 # 전체 수정 (모든 필드 필요)
PATCH /users/123 # 부분 수정 (일부 필드만)
Body: { "name": "김철수" }
# Delete - DELETE
DELETE /users/123 # 삭제
# 멱등성 O (여러 번 호출해도 같은 결과)
GET /users/123 # ✅ 항상 같은 결과
PUT /users/123 # ✅ 같은 데이터로 여러 번 호출해도 같은 상태
DELETE /users/123 # ✅ 두 번째부터는 404지만 상태는 같음
# 멱등성 X (호출할 때마다 결과가 다름)
POST /users # ❌ 호출할 때마다 새 사용자 생성
PATCH /orders/123/add-item # ❌ 호출할 때마다 아이템 추가
# 성공 (2xx)
200 OK # GET, PUT, PATCH 성공
201 Created # POST 성공 (Location 헤더와 함께)
204 No Content # DELETE 성공 (응답 body 없음)
# 클라이언트 오류 (4xx)
400 Bad Request # 잘못된 요청 (validation 실패)
401 Unauthorized # 인증 필요
403 Forbidden # 권한 없음
404 Not Found # 리소스 없음
409 Conflict # 중복 등 충돌
# 서버 오류 (5xx)
500 Internal Server Error # 서버 오류
503 Service Unavailable # 일시적 서비스 불가
# 불필요한 세분화
451 Unavailable For Legal Reasons # ❌ 너무 구체적
403 Forbidden # ✅ 충분함
# 일관성 없는 사용
POST /users → 200 OK (이미 있음) # ❌
POST /users → 409 Conflict # ✅
// ✅ 성공 응답
{
"data": {
"id": "123",
"name": "홍길동",
"email": "hong@example.com"
}
}
// ✅ 목록 응답
{
"data": [
{ "id": "1", "name": "홍길동" },
{ "id": "2", "name": "김철수" }
],
"pagination": {
"page": 1,
"pageSize": 20,
"totalCount": 100,
"totalPages": 5
}
}
// ✅ 에러 응답
{
"error": {
"code": "VALIDATION_ERROR",
"message": "이메일 형식이 올바르지 않습니다",
"details": [
{
"field": "email",
"message": "유효한 이메일을 입력하세요"
}
]
}
}
// ❌ 불일치한 구조
// 성공 시
{ "user": { "id": 1 } }
// 실패 시
{ "error": "..." }
// ✅ 일관된 구조
// 항상 data 또는 error 키 사용
GET /users?page=2&pageSize=20
Response:
{
"data": [...],
"pagination": {
"page": 2,
"pageSize": 20,
"totalCount": 150,
"totalPages": 8,
"hasNext": true,
"hasPrev": true
}
}
GET /posts?cursor=eyJpZCI6MTIzfQ&limit=20
Response:
{
"data": [...],
"pagination": {
"nextCursor": "eyJpZCI6MTQzfQ",
"prevCursor": "eyJpZCI6MTAzfQ",
"hasMore": true
}
}
# 필터링
GET /products?category=electronics&minPrice=10000&maxPrice=50000
# 정렬
GET /products?sort=price # 오름차순
GET /products?sort=-price # 내림차순 (- 접두사)
GET /products?sort=price,-rating # 다중 정렬
# 필드 선택 (Sparse Fieldsets)
GET /users?fields=id,name,email # 필요한 필드만
# 검색
GET /products?q=노트북 # 통합 검색
GET /products?search=name:노트북 # 필드별 검색
# 헤더에 토큰 전달
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
# ❌ URL에 토큰 포함하지 않기
GET /users?token=xxx # 위험! 로그에 남음
# 헤더 사용 (권장)
X-API-Key: your-api-key-here
# 또는 쿼리 파라미터 (읽기 전용 API만)
GET /public/data?apiKey=xxx
GET /v1/users
GET /v2/users
# 장점: 명확하고 캐싱 쉬움
# 단점: URL이 길어짐
GET /users
Accept: application/vnd.myapp.v2+json
# 장점: URL 깔끔
# 단점: 브라우저 테스트 어려움
# 하위 호환성 유지
v1: { "name": "홍길동" }
v2: { "name": "홍길동", "firstName": "길동", "lastName": "홍" } # ✅
v1: { "name": "홍길동" }
v2: { "fullName": "홍길동" } # ❌ name 필드 제거 (breaking change)
# 사용자의 주문 조회
GET /users/123/orders
# 특정 주문 상세
GET /users/123/orders/456
# 모든 주문에서 필터링
GET /orders?userId=123
# 언제 사용?
# - 리소스가 독립적으로 관리될 때
# - 다양한 필터링이 필요할 때
// ✅ 좋은 에러 응답
{
"error": {
"code": "VALIDATION_ERROR",
"message": "입력값을 확인해주세요",
"details": [
{
"field": "email",
"code": "INVALID_FORMAT",
"message": "올바른 이메일 형식이 아닙니다"
},
{
"field": "password",
"code": "TOO_SHORT",
"message": "비밀번호는 8자 이상이어야 합니다"
}
],
"requestId": "req_123abc"
}
}
// ❌ 나쁜 에러 응답
{
"error": "Error" // 너무 모호함
}
# 명명 규칙
VALIDATION_ERROR
RESOURCE_NOT_FOUND
AUTHENTICATION_REQUIRED
INSUFFICIENT_PERMISSIONS
RATE_LIMIT_EXCEEDED
# ❌ 피할 것
ERR_001 # 의미 불명확
error # 너무 일반적
HTTP/1.1 200 OK
X-RateLimit-Limit: 100 # 시간당 한도
X-RateLimit-Remaining: 87 # 남은 요청 수
X-RateLimit-Reset: 1640000000 # 리셋 시각 (Unix timestamp)
# 한도 초과 시
HTTP/1.1 429 Too Many Requests
Retry-After: 3600 # 초 단위
# ❌ 나쁜 설계
GET /getUser?id=123
POST /createUser
POST /updateUser
POST /deleteUser
# ✅ 좋은 설계
GET /users/123
POST /users
PUT /users/123
DELETE /users/123
# ❌ 나쁜 설계
GET /search/users/홍길동
# ✅ 좋은 설계
GET /users?q=홍길동
# ❌ 나쁜 설계
POST /orders/123/sendEmail
# ✅ 좋은 설계
POST /orders/123/notifications
Body: { "type": "email" }
# 또는 이벤트 기반
POST /orders/123/confirm # 확인하면 자동으로 이메일 발송
# URL 인코딩
GET /products?q=%ED%99%8D%EA%B8%B8%EB%8F%99
# Accept-Language 헤더
GET /products
Accept-Language: ko-KR
# 응답에 언어 포함
{
"data": {
"name": "상품명",
"nameEn": "Product Name"
}
}
# ❌ URL에 포함하지 않기
GET /users?residentNumber=123456-1234567
# ✅ POST body에 포함
POST /users/verify
Body: { "residentNumber": "123456-1234567" }
# 응답에서 마스킹
{
"residentNumber": "123456-1******"
}
"API는 계약이다. 한 번 배포하면 쉽게 바꿀 수 없다."