JSON API 디자인 패턴: 더 나은 REST API 구축
잘 설계된 API는 사용하기 즐겁습니다. 잘못 설계된 것은 버그, 불만, 기술 부채를 만듭니다. 이 가이드에서는 일관되고 발견 가능하며 유지보수 가능한 JSON REST API를 설계하기 위한 실전 검증된 패턴을 다룹니다.
리소스 명명
리소스는 동사가 아닌 명사입니다. 컬렉션에는 복수 명사를, 개별 리소스에는 ID를 사용하세요:
GET /api/users # 사용자 목록
POST /api/users # 사용자 생성
GET /api/users/123 # 사용자 123 조회
PUT /api/users/123 # 사용자 123 수정
DELETE /api/users/123 # 사용자 123 삭제
명명 규칙:
- 소문자와 하이픈 사용:
/api/blog-posts(blogPosts나blog_posts가 아님) - 관련 리소스 중첩:
/api/users/123/orders - 중첩을 2단계로 제한:
/api/users/123/orders/456(더 깊이는 안 됨) - URL에 동사 피하기: CRUD에 매핑되지 않는 액션에
/api/users/123/activate는 허용
응답 엔벨로프
응답을 일관된 엔벨로프로 감싸세요:
{
"data": {
"id": "123",
"type": "user",
"attributes": {
"name": "Alice",
"email": "alice@example.com",
"createdAt": "2024-01-15T10:30:00Z"
}
},
"meta": {
"requestId": "req_abc123"
}
}
컬렉션의 경우:
{
"data": [
{ "id": "123", "name": "Alice" },
{ "id": "456", "name": "Bob" }
],
"meta": {
"total": 142,
"page": 1,
"perPage": 20
}
}
API 응답이 스키마와 일치하는지 JSON 유효성 검사기로 확인하세요.
페이지네이션
세 가지 일반적인 접근 방식:
오프셋 기반 (가장 간단)
GET /api/users?page=2&per_page=20
{
"data": [...],
"meta": {
"page": 2,
"perPage": 20,
"total": 142,
"totalPages": 8
}
}
장점: 간단하고 임의 페이지 이동 지원. 단점: 동시 삽입/삭제 시 일관되지 않은 결과.
커서 기반 (가장 안정적)
GET /api/users?cursor=eyJpZCI6MTIzfQ&limit=20
{
"data": [...],
"meta": {
"hasNext": true,
"nextCursor": "eyJpZCI6MTQzfQ"
}
}
장점: 동시 변경에도 일관적, 대규모 데이터셋에서 성능 좋음. 단점: 임의 페이지로 이동 불가.
키셋 기반 (가장 성능 좋음)
GET /api/users?after_id=123&limit=20
마지막 항목의 ID(또는 다른 정렬 필드)를 사용하여 다음 페이지를 가져옵니다. 커서 기반과 유사하지만 매개변수가 투명합니다.
권장: 실시간 피드와 대규모 데이터셋에는 커서 기반. 페이지 이동이 중요한 관리 대시보드에는 오프셋 기반.
필터링과 정렬
필터링
GET /api/users?status=active&role=admin
GET /api/users?created_after=2024-01-01
GET /api/users?search=alice
복잡한 필터에는 전용 쿼리 매개변수를 고려하세요:
GET /api/users?filter[status]=active&filter[role]=admin
정렬
GET /api/users?sort=name # 오름차순
GET /api/users?sort=-created_at # 내림차순 (- 접두사)
GET /api/users?sort=-created_at,name # 다중 필드
오류 처리
일관된 오류 응답은 API 사용성에 매우 중요합니다:
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{
"field": "email",
"message": "Must be a valid email address",
"value": "not-an-email"
},
{
"field": "age",
"message": "Must be between 0 and 150",
"value": -5
}
]
},
"meta": {
"requestId": "req_abc123"
}
}
사용할 HTTP 상태 코드:
| 코드 | 의미 | 사용 시점 |
|---|---|---|
| 200 | OK | 성공적인 GET, PUT |
| 201 | Created | 성공적인 POST |
| 204 | No Content | 성공적인 DELETE |
| 400 | Bad Request | 유효성 검사 오류 |
| 401 | Unauthorized | 인증 누락/유효하지 않음 |
| 403 | Forbidden | 유효한 인증, 불충분한 권한 |
| 404 | Not Found | 리소스가 존재하지 않음 |
| 409 | Conflict | 중복 리소스, 버전 충돌 |
| 422 | Unprocessable | 의미론적으로 유효하지 않음 |
| 429 | Too Many Requests | 요청 제한 초과 |
| 500 | Internal Error | 예상치 못한 서버 오류 |
버전 관리
세 가지 접근 방식:
URL 경로 (권장)
GET /api/v1/users
GET /api/v2/users
장점: 명시적, 라우팅 쉬움, 테스트 쉬움.
헤더 기반
GET /api/users
Accept: application/vnd.myapi.v2+json
장점: 깔끔한 URL. 단점: 테스트 어려움, 발견하기 어려움.
쿼리 매개변수
GET /api/users?version=2
장점: 테스트 쉬움. 단점: 선택적 매개변수 의미론.
권장: URL 경로 버전 관리가 가장 실용적입니다. 명시적이고, 캐시 가능하며, 모든 HTTP 도구에서 작동합니다.
날짜 및 시간 처리
항상 UTC의 ISO 8601을 사용하세요:
{
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T14:22:33Z",
"expiresAt": "2024-12-31T23:59:59Z"
}
API 응답에 Unix 타임스탬프를 절대 사용하지 마세요 — 모호하고(초 vs 밀리초) 사람이 읽을 수 없습니다. 타임스탬프 처리에 대한 자세한 내용은 Unix 타임스탬프 가이드를 참조하세요.
요청 제한
응답 헤더에 요청 제한을 전달하세요:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 67
X-RateLimit-Reset: 1705312800
Retry-After: 30
제한 초과 시 429 Too Many Requests를 반환하고, 명확한 오류 메시지와 Retry-After 헤더를 포함하세요.
HATEOAS (선택이지만 강력)
관련 리소스와 액션에 대한 링크를 포함하세요:
{
"data": {
"id": "123",
"name": "Alice",
"links": {
"self": "/api/users/123",
"orders": "/api/users/123/orders",
"avatar": "/api/users/123/avatar"
}
}
}
이를 통해 API가 자체 문서화되고 클라이언트 측 URL 구성이 줄어듭니다.
자주 묻는 질문
JSON:API 사양을 사용해야 하나요, 아니면 자체 포맷을 설계해야 하나요?
JSON:API 사양 (jsonapi.org)은 포괄적인 표준을 제공하지만 간단한 API에는 장황할 수 있습니다. 대부분의 프로젝트에서 이 가이드의 패턴을 따르는 더 간단한 사용자 정의 포맷을 설계하는 것이 더 실용적입니다. 자동 클라이언트 라이브러리와 엄격한 표준이 필요한 경우 JSON:API를 사용하세요.
부분 업데이트를 어떻게 처리해야 하나요 (PATCH vs PUT)?
완전한 리소스 교체에는 PUT을 사용하세요 (클라이언트가 모든 필드를 보냄). 부분 업데이트에는 PATCH를 사용하세요 (클라이언트가 변경된 필드만 보냄). JSON Merge Patch (RFC 7396)를 사용한 PATCH가 가장 간단한 접근 방식입니다: 업데이트할 필드만 있는 JSON 객체를 보내고, 필드를 제거하려면 null을 보냅니다.
관련 리소스
- JSON 포맷터 — 가독성을 위해 API 응답 포맷
- JSON Schema 유효성 검사 가이드 — JSON Schema로 API 페이로드 유효성 검사
- JWT 토큰 설명 — JSON Web Token으로 API 보안