
RESTful API 설계 원칙과 실전 베스트 프랙티스를 통해 확장 가능하고 유지보수하기 쉬운 API를 설계하는 방법을 알아봅니다.
REST(Representational State Transfer)는 현대 웹 API 설계의 표준으로 자리잡았습니다. 잘 설계된 RESTful API는 직관적이고, 확장 가능하며, 유지보수하기 쉽습니다. 이 글에서는 REST API 설계의 핵심 원칙과 실전 베스트 프랙티스를 다룹니다.
REST 아키텍처는 6가지 제약 조건을 기반으로 합니다:
Client-Server 분리 클라이언트와 서버는 독립적으로 진화할 수 있어야 합니다. 이를 통해 각 계층의 관심사를 분리하고 확장성을 높입니다.
Stateless(무상태성) 각 요청은 독립적이며 서버는 클라이언트의 상태를 저장하지 않습니다. 모든 필요한 정보는 요청에 포함되어야 합니다.
# ✅ Good: 모든 정보가 요청에 포함됨
GET /api/users/123/orders?status=pending
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
# ❌ Bad: 서버 세션에 의존
GET /api/orders
Cookie: sessionId=abc123Cacheable(캐시 가능) 응답은 캐시 가능 여부를 명시해야 하며, 이를 통해 성능과 확장성을 개선합니다.
Uniform Interface(일관된 인터페이스) 리소스 식별, 표현을 통한 리소스 조작, 자기 서술적 메시지, HATEOAS 등의 원칙을 따릅니다.
Layered System(계층화된 시스템) 클라이언트는 직접 연결된 서버 외의 다른 계층(로드 밸런서, 캐시, 게이트웨이)을 알 필요가 없습니다.
Code on Demand(선택적) 서버는 실행 가능한 코드(예: JavaScript)를 클라이언트에 전송할 수 있습니다.
RESTful API는 동사가 아닌 명사를 사용하여 리소스를 표현합니다. HTTP 메서드가 이미 동작을 나타내기 때문입니다.
# ✅ Good: 명사 사용
GET /api/users
POST /api/users
GET /api/users/123
PUT /api/users/123
DELETE /api/users/123
# ❌ Bad: 동사 사용
GET /api/getUsers
POST /api/createUser
GET /api/getUserById/123
POST /api/updateUser/123
POST /api/deleteUser/123관계가 있는 리소스는 URL 경로로 명확하게 표현합니다:
# 사용자의 주문 목록
GET /api/users/123/orders
# 특정 주문의 상세 정보
GET /api/users/123/orders/456
# 주문의 배송 정보
GET /api/orders/456/shipping
# 사용자의 주문 생성
POST /api/users/123/orders
Content-Type: application/json
{
"items": [
{"productId": "prod-001", "quantity": 2}
],
"shippingAddress": "서울시 강남구..."
}복수형과 단수형을 일관되게 사용합니다:
# ✅ Good: 일관된 복수형 사용
GET /api/users # 컬렉션
GET /api/users/123 # 단일 리소스
GET /api/users/123/posts # 중첩된 컬렉션
# ❌ Bad: 혼재된 사용
GET /api/user # 단수형
GET /api/users/123 # 복수형
GET /api/user/123/post # 혼재각 메서드는 명확한 의미와 특성을 가집니다:
GET - 리소스 조회
GET /api/users/123
Accept: application/json
# Response: 200 OK
{
"id": 123,
"name": "김철수",
"email": "kim@example.com",
"createdAt": "2024-01-15T09:00:00Z"
}POST - 리소스 생성
POST /api/users
Content-Type: application/json
{
"name": "이영희",
"email": "lee@example.com"
}
# Response: 201 Created
Location: /api/users/124
{
"id": 124,
"name": "이영희",
"email": "lee@example.com",
"createdAt": "2025-10-01T10:30:00Z"
}PUT - 리소스 전체 수정
PUT /api/users/123
Content-Type: application/json
{
"name": "김철수",
"email": "kim.updated@example.com",
"phone": "010-1234-5678"
}
# Response: 200 OK
{
"id": 123,
"name": "김철수",
"email": "kim.updated@example.com",
"phone": "010-1234-5678",
"updatedAt": "2025-10-01T11:00:00Z"
}PATCH - 리소스 부분 수정
PATCH /api/users/123
Content-Type: application/json
{
"email": "kim.new@example.com"
}
# Response: 200 OK
{
"id": 123,
"name": "김철수",
"email": "kim.new@example.com",
"phone": "010-1234-5678",
"updatedAt": "2025-10-01T11:15:00Z"
}DELETE - 리소스 삭제
DELETE /api/users/123
# Response: 204 No Content
# 또는
# Response: 200 OK
{
"message": "User successfully deleted"
}검색이나 복잡한 쿼리는 POST를 사용할 수 있습니다:
# 간단한 검색: GET 사용
GET /api/users?search=김철수&role=admin
# 복잡한 검색: POST 사용 (body에 복잡한 조건)
POST /api/users/search
Content-Type: application/json
{
"filters": {
"age": {"min": 20, "max": 30},
"tags": ["developer", "designer"],
"registeredAfter": "2024-01-01"
},
"sort": {"field": "createdAt", "order": "desc"},
"pagination": {"page": 1, "size": 20}
}# 200 OK - 요청 성공 (GET, PUT, PATCH)
GET /api/users/123
Response: 200 OK
# 201 Created - 리소스 생성 성공 (POST)
POST /api/users
Response: 201 Created
Location: /api/users/124
# 204 No Content - 성공했으나 반환할 내용 없음 (DELETE)
DELETE /api/users/123
Response: 204 No Content# 400 Bad Request - 잘못된 요청 형식
POST /api/users
{
"email": "invalid-email" # 이메일 형식 오류
}
Response: 400 Bad Request
{
"error": "ValidationError",
"message": "Invalid email format",
"details": {
"field": "email",
"value": "invalid-email"
}
}
# 401 Unauthorized - 인증 필요
GET /api/users/me
Response: 401 Unauthorized
{
"error": "Unauthorized",
"message": "Authentication required"
}
# 403 Forbidden - 권한 없음 (인증은 되었으나 접근 불가)
DELETE /api/users/999
Response: 403 Forbidden
{
"error": "Forbidden",
"message": "You don't have permission to delete this user"
}
# 404 Not Found - 리소스를 찾을 수 없음
GET /api/users/99999
Response: 404 Not Found
{
"error": "NotFound",
"message": "User with id 99999 not found"
}
# 409 Conflict - 리소스 충돌
POST /api/users
{
"email": "existing@example.com"
}
Response: 409 Conflict
{
"error": "Conflict",
"message": "User with this email already exists"
}
# 422 Unprocessable Entity - 의미론적 오류
POST /api/orders
{
"items": [], # 빈 주문
"total": -100 # 음수 금액
}
Response: 422 Unprocessable Entity
{
"error": "ValidationError",
"message": "Order validation failed",
"details": [
{"field": "items", "message": "Order must contain at least one item"},
{"field": "total", "message": "Total amount must be positive"}
]
}
# 429 Too Many Requests - 요청 제한 초과
GET /api/users
Response: 429 Too Many Requests
Retry-After: 60
{
"error": "RateLimitExceeded",
"message": "Too many requests. Please try again in 60 seconds."
}# 500 Internal Server Error - 서버 내부 오류
GET /api/users
Response: 500 Internal Server Error
{
"error": "InternalServerError",
"message": "An unexpected error occurred",
"requestId": "req-123456" # 로그 추적용
}
# 503 Service Unavailable - 서비스 일시 중단
GET /api/users
Response: 503 Service Unavailable
Retry-After: 300
{
"error": "ServiceUnavailable",
"message": "Service is temporarily unavailable due to maintenance"
}# Offset-based pagination
GET /api/users?page=2&size=20
Response: 200 OK
{
"data": [...],
"pagination": {
"page": 2,
"size": 20,
"total": 150,
"totalPages": 8
}
}
# Cursor-based pagination (대용량 데이터에 적합)
GET /api/users?cursor=eyJpZCI6MTIzfQ&limit=20
Response: 200 OK
{
"data": [...],
"pagination": {
"nextCursor": "eyJpZCI6MTQzfQ",
"hasMore": true
}
}# 정렬
GET /api/users?sort=createdAt:desc,name:asc
# 필터링
GET /api/users?role=admin&status=active
# 필드 선택 (부분 응답)
GET /api/users/123?fields=id,name,email
Response: 200 OK
{
"id": 123,
"name": "김철수",
"email": "kim@example.com"
# phone, address 등 다른 필드는 제외됨
}
# 검색
GET /api/users?search=김철수
# 복합 쿼리
GET /api/products?category=electronics&minPrice=100000&maxPrice=500000&sort=price:asc&page=1&size=20URL 경로 버전 관리 (권장)
GET /api/v1/users
GET /api/v2/users
# 장점: 명확하고 캐시하기 쉬움
# 단점: URL이 길어짐헤더 버전 관리
GET /api/users
Accept: application/vnd.myapi.v1+json
# 장점: URL이 깔끔함
# 단점: 디버깅이 어려움쿼리 파라미터 버전 관리
GET /api/users?version=1
# 장점: 간단함
# 단점: 캐싱 복잡도 증가# v1: 기본 사용자 정보
GET /api/v1/users/123
Response: 200 OK
{
"id": 123,
"name": "김철수",
"email": "kim@example.com"
}
# v2: 확장된 정보 (하위 호환 유지)
GET /api/v2/users/123
Response: 200 OK
{
"id": 123,
"name": "김철수",
"email": "kim@example.com",
"profile": {
"avatar": "https://...",
"bio": "..."
},
"settings": {
"language": "ko",
"timezone": "Asia/Seoul"
}
}# 표준 에러 형식
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"details": [
{
"field": "email",
"message": "Invalid email format",
"value": "invalid-email"
},
{
"field": "age",
"message": "Must be at least 18",
"value": 15
}
],
"requestId": "req-abc123",
"timestamp": "2025-10-01T12:00:00Z"
}
}// 에러 코드 정의 예시
enum ErrorCode {
// 인증/인가
UNAUTHORIZED = 'UNAUTHORIZED',
FORBIDDEN = 'FORBIDDEN',
TOKEN_EXPIRED = 'TOKEN_EXPIRED',
// 리소스
NOT_FOUND = 'NOT_FOUND',
ALREADY_EXISTS = 'ALREADY_EXISTS',
// 검증
VALIDATION_ERROR = 'VALIDATION_ERROR',
INVALID_FORMAT = 'INVALID_FORMAT',
// 비즈니스 로직
INSUFFICIENT_BALANCE = 'INSUFFICIENT_BALANCE',
ORDER_ALREADY_SHIPPED = 'ORDER_ALREADY_SHIPPED',
// 시스템
INTERNAL_ERROR = 'INTERNAL_ERROR',
SERVICE_UNAVAILABLE = 'SERVICE_UNAVAILABLE',
RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED'
}openapi: 3.0.0
info:
title: User Management API
version: 1.0.0
description: RESTful API for user management
paths:
/api/users:
get:
summary: Get all users
parameters:
- name: page
in: query
schema:
type: integer
default: 1
- name: size
in: query
schema:
type: integer
default: 20
responses:
'200':
description: Successful response
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/User'
pagination:
$ref: '#/components/schemas/Pagination'
components:
schemas:
User:
type: object
properties:
id:
type: integer
name:
type: string
email:
type: string
format: email문서에는 실제 사용 예시를 포함해야 합니다:
# 사용자 생성 예시
curl -X POST https://api.example.com/api/users \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_TOKEN" \
-d '{
"name": "김철수",
"email": "kim@example.com"
}'
# 응답 예시
# {
# "id": 123,
# "name": "김철수",
# "email": "kim@example.com",
# "createdAt": "2025-10-01T12:00:00Z"
# }# JWT 토큰 기반 인증
GET /api/users/me
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
# API 키 인증
GET /api/users
X-API-Key: your-api-key-here
# OAuth 2.0
GET /api/users
Authorization: Bearer oauth-access-token# Rate limit 헤더 포함
GET /api/users
Response: 200 OK
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1633024800
# 제한 초과 시
Response: 429 Too Many Requests
Retry-After: 60
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1633024860// 입력 검증 예시 (Express.js + Joi)
import Joi from 'joi';
const userSchema = Joi.object({
name: Joi.string().min(2).max(50).required(),
email: Joi.string().email().required(),
age: Joi.number().min(18).max(120),
password: Joi.string()
.min(8)
.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
.required()
});
app.post('/api/users', async (req, res) => {
try {
const validated = await userSchema.validateAsync(req.body);
// 검증된 데이터로 처리
} catch (error) {
res.status(400).json({
error: 'ValidationError',
message: error.message
});
}
});REST API를 설계할 때 다음 체크리스트를 확인하세요:
리소스 설계
HTTP 메서드
상태 코드
보안
문서화
성능
REST API 설계는 단순히 데이터를 주고받는 것 이상입니다. 직관적이고, 일관되며, 확장 가능한 API를 만들기 위해서는 RESTful 원칙을 이해하고 베스트 프랙티스를 따라야 합니다.
핵심은 예측 가능성입니다. 개발자가 API 문서를 최소한으로 참고하면서도 직관적으로 사용할 수 있어야 합니다. 리소스 중심 설계, 표준 HTTP 메서드와 상태 코드 활용, 일관된 명명 규칙, 그리고 철저한 문서화가 이를 가능하게 합니다.
잘 설계된 REST API는 프론트엔드 개발자, 모바일 개발자, 그리고 서드파티 통합을 원하는 모든 개발자에게 즐거운 경험을 제공합니다. 이 가이드가 여러분의 API 설계에 도움이 되기를 바랍니다.