NAVIGATION
  • 전체 글
CATEGORIES
  • Develop7
    • Backend3
    • Frontend2
    • Git1
    • Performance1
  • AI3
    • Claude Code3
  • Frontend1
    • Next.js1
POPULAR TAGS
  • API3
  • 클로드코드2
  • 성능최적화2
  • GraphQL2
  • REST2
  • React2
  • JavaScript2
  • 프론트엔드2
  • Claude Code1
  • AI1
···
>steady-one_

© 2025 steady-one Blog. All rights reserved.

REST API 설계 베스트 프랙티스
Backend
2025년 10월 1일

REST API 설계 베스트 프랙티스

RESTful API 설계 원칙과 실전 베스트 프랙티스를 통해 확장 가능하고 유지보수하기 쉬운 API를 설계하는 방법을 알아봅니다.

.rest api.api.백엔드

REST API 설계 베스트 프랙티스

REST(Representational State Transfer)는 현대 웹 API 설계의 표준으로 자리잡았습니다. 잘 설계된 RESTful API는 직관적이고, 확장 가능하며, 유지보수하기 쉽습니다. 이 글에서는 REST API 설계의 핵심 원칙과 실전 베스트 프랙티스를 다룹니다.

1. RESTful 원칙의 이해

1.1 REST의 핵심 제약 조건

REST 아키텍처는 6가지 제약 조건을 기반으로 합니다:

Client-Server 분리 클라이언트와 서버는 독립적으로 진화할 수 있어야 합니다. 이를 통해 각 계층의 관심사를 분리하고 확장성을 높입니다.

Stateless(무상태성) 각 요청은 독립적이며 서버는 클라이언트의 상태를 저장하지 않습니다. 모든 필요한 정보는 요청에 포함되어야 합니다.

# ✅ Good: 모든 정보가 요청에 포함됨
GET /api/users/123/orders?status=pending
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
 
# ❌ Bad: 서버 세션에 의존
GET /api/orders
Cookie: sessionId=abc123

Cacheable(캐시 가능) 응답은 캐시 가능 여부를 명시해야 하며, 이를 통해 성능과 확장성을 개선합니다.

Uniform Interface(일관된 인터페이스) 리소스 식별, 표현을 통한 리소스 조작, 자기 서술적 메시지, HATEOAS 등의 원칙을 따릅니다.

Layered System(계층화된 시스템) 클라이언트는 직접 연결된 서버 외의 다른 계층(로드 밸런서, 캐시, 게이트웨이)을 알 필요가 없습니다.

Code on Demand(선택적) 서버는 실행 가능한 코드(예: JavaScript)를 클라이언트에 전송할 수 있습니다.

2. 리소스 중심 엔드포인트 설계

2.1 명사 사용과 계층 구조

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

2.2 리소스 관계 표현

관계가 있는 리소스는 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": "서울시 강남구..."
}

2.3 컬렉션과 단일 리소스

복수형과 단수형을 일관되게 사용합니다:

# ✅ 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   # 혼재

3. HTTP 메서드의 올바른 사용

3.1 주요 HTTP 메서드

각 메서드는 명확한 의미와 특성을 가집니다:

GET - 리소스 조회

  • 안전(Safe): 서버 상태를 변경하지 않음
  • 멱등성(Idempotent): 여러 번 호출해도 결과가 동일
  • 캐시 가능
GET /api/users/123
Accept: application/json
 
# Response: 200 OK
{
  "id": 123,
  "name": "김철수",
  "email": "kim@example.com",
  "createdAt": "2024-01-15T09:00:00Z"
}

POST - 리소스 생성

  • 비안전(Unsafe): 서버 상태 변경
  • 비멱등성: 여러 번 호출 시 여러 리소스 생성 가능
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"
}

3.2 특수한 경우의 처리

검색이나 복잡한 쿼리는 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}
}

4. HTTP 상태 코드 가이드

4.1 성공 응답 (2xx)

# 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

4.2 클라이언트 오류 (4xx)

# 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."
}

4.3 서버 오류 (5xx)

# 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"
}

5. 쿼리 파라미터와 필터링

5.1 페이지네이션

# 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
  }
}

5.2 정렬과 필터링

# 정렬
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=20

6. API 버전 관리

6.1 버전 관리 전략

URL 경로 버전 관리 (권장)

GET /api/v1/users
GET /api/v2/users
 
# 장점: 명확하고 캐시하기 쉬움
# 단점: URL이 길어짐

헤더 버전 관리

GET /api/users
Accept: application/vnd.myapi.v1+json
 
# 장점: URL이 깔끔함
# 단점: 디버깅이 어려움

쿼리 파라미터 버전 관리

GET /api/users?version=1
 
# 장점: 간단함
# 단점: 캐싱 복잡도 증가

6.2 하위 호환성 유지

# 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"
  }
}

7. 에러 처리 표준화

7.1 일관된 에러 응답 형식

# 표준 에러 형식
{
  "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"
  }
}

7.2 에러 코드 체계

// 에러 코드 정의 예시
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'
}

8. API 문서화

8.1 OpenAPI (Swagger) 스펙

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

8.2 예시 코드와 시나리오

문서에는 실제 사용 예시를 포함해야 합니다:

# 사용자 생성 예시
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"
# }

9. 보안 베스트 프랙티스

9.1 인증과 인가

# 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

9.2 Rate Limiting

# 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

9.3 입력 검증과 Sanitization

// 입력 검증 예시 (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
    });
  }
});

10. 실전 체크리스트

REST API를 설계할 때 다음 체크리스트를 확인하세요:

리소스 설계

  • 명사를 사용한 리소스 명명
  • 복수형 일관성 유지
  • 계층적 관계 명확히 표현
  • URL 깊이 3단계 이내 유지

HTTP 메서드

  • GET: 조회만 수행 (멱등성)
  • POST: 리소스 생성
  • PUT: 전체 업데이트 (멱등성)
  • PATCH: 부분 업데이트
  • DELETE: 리소스 삭제 (멱등성)

상태 코드

  • 적절한 2xx, 4xx, 5xx 코드 사용
  • 에러 시 명확한 메시지 제공
  • 일관된 에러 응답 형식

보안

  • HTTPS 사용
  • 인증/인가 구현
  • Rate limiting 적용
  • 입력 검증 및 sanitization
  • CORS 적절히 설정

문서화

  • OpenAPI/Swagger 스펙 작성
  • 예시 코드 제공
  • 에러 코드 문서화
  • 변경 사항 버전 관리

성능

  • 페이지네이션 구현
  • 캐싱 전략 수립
  • 부분 응답 지원
  • 압축(gzip) 활성화

결론

REST API 설계는 단순히 데이터를 주고받는 것 이상입니다. 직관적이고, 일관되며, 확장 가능한 API를 만들기 위해서는 RESTful 원칙을 이해하고 베스트 프랙티스를 따라야 합니다.

핵심은 예측 가능성입니다. 개발자가 API 문서를 최소한으로 참고하면서도 직관적으로 사용할 수 있어야 합니다. 리소스 중심 설계, 표준 HTTP 메서드와 상태 코드 활용, 일관된 명명 규칙, 그리고 철저한 문서화가 이를 가능하게 합니다.

잘 설계된 REST API는 프론트엔드 개발자, 모바일 개발자, 그리고 서드파티 통합을 원하는 모든 개발자에게 즐거운 경험을 제공합니다. 이 가이드가 여러분의 API 설계에 도움이 되기를 바랍니다.

목차

  • REST API 설계 베스트 프랙티스
  • 1. RESTful 원칙의 이해
  • 2. 리소스 중심 엔드포인트 설계
  • 3. HTTP 메서드의 올바른 사용
  • 4. HTTP 상태 코드 가이드
  • 5. 쿼리 파라미터와 필터링
  • 6. API 버전 관리
  • 7. 에러 처리 표준화
  • 8. API 문서화
  • 9. 보안 베스트 프랙티스
  • 10. 실전 체크리스트
  • 결론