DevToolBox무료
블로그

REST API 모범 사례: 2026년 완전 가이드

15분 읽기by DevToolBox

잘 설계된 REST API를 구축하는 것은 현대 개발자에게 가장 중요한 기술 중 하나입니다. 훌륭한 API는 직관적이고, 일관되며, 안전하고, 유지보수가 쉽습니다. 이 종합 가이드는 URL 명명 규칙과 HTTP 메서드부터 인증, 페이지네이션, 속도 제한, 일반적인 실수까지 모든 것을 다룹니다.

URL 명명 규칙

좋은 REST API의 기반은 잘 설계된 URL에서 시작됩니다. 엔드포인트는 예측 가능하고, 읽기 쉬우며, 일관된 패턴을 따라야 합니다.

동사가 아닌 명사 사용

REST 리소스는 명사로 식별되어야 합니다. HTTP 메서드가 이미 동작을 나타내므로 URL 경로에 동사를 포함할 필요가 없습니다.

# Good - nouns as resources
GET    /users          # Get all users
GET    /users/123      # Get user 123
POST   /users          # Create a new user
DELETE /users/123      # Delete user 123

# Bad - verbs in URLs
GET    /getUsers
POST   /createUser
DELETE /deleteUser/123

컬렉션에는 복수형 사용

컬렉션 엔드포인트에는 항상 복수 명사를 사용하세요. 이렇게 하면 컬렉션 전체나 그 안의 단일 리소스를 참조할 때 일관성이 유지됩니다.

# Good - consistent plurals
GET /users          # Collection
GET /users/123      # Single resource in collection
GET /products       # Collection
GET /products/456   # Single resource

# Bad - inconsistent or singular
GET /user           # Ambiguous: one user or all users?
GET /user/123
GET /product-list

관계를 위한 중첩 리소스

중첩을 사용하여 리소스 간의 관계를 표현하되, 2단계 이상 깊이 들어가지 마세요.

# Good - clear parent-child relationship (max 2 levels)
GET /users/123/orders          # Orders belonging to user 123
GET /users/123/orders/456      # Specific order of user 123

# Bad - too deeply nested
GET /users/123/orders/456/items/789/reviews
# Better: use a direct resource endpoint
GET /order-items/789/reviews
# Or: flatten with query parameters
GET /reviews?order_item_id=789

URL 패턴: 좋은 예 vs 나쁜 예

좋은 예나쁜 예이유
GET /usersGET /getUsersHTTP method already implies the action
GET /users/123GET /user?id=123Use path parameters for resource identity
POST /usersPOST /createUserPOST already means create
PUT /users/123POST /updateUserPUT/PATCH for updates, use resource path
DELETE /users/123POST /deleteUserUse DELETE method, not POST with verb
GET /users/123/ordersGET /getUserOrders?userId=123Use nested resources for relationships
GET /products?category=electronicsGET /electronics/productsUse query params for filtering
GET /users?status=active&sort=nameGET /active-users-sorted-by-nameUse query params, not encoded URLs
PATCH /users/123POST /users/123/updateUse HTTP methods, not URL verbs
GET /users/123/avatarGET /getAvatarForUser/123Keep it resource-oriented

HTTP 메서드

각 HTTP 메서드에는 특정 의미가 있습니다. 올바르게 사용하면 API가 예측 가능해집니다.

메서드목적멱등성요청 본문예시
GETRetrieve a resource or collectionYesNoGET /users/123
POSTCreate a new resourceNoYesPOST /users
PUTReplace a resource entirelyYesYesPUT /users/123
PATCHPartially update a resourceNo*YesPATCH /users/123
DELETERemove a resourceYesOptionalDELETE /users/123

* PATCH can be idempotent depending on implementation. If the patch describes the final state (e.g., {"name": "John"}), it is idempotent. If it describes an operation (e.g., {"op": "increment", "path": "/count"}), it is not.

HTTP 상태 코드

올바른 상태 코드를 사용하면 클라이언트가 응답을 적절하게 처리할 수 있습니다.

코드이름사용 시기
200OKSuccessful GET, PUT, PATCH, or DELETE. Return the resource or confirmation.
201CreatedSuccessful POST that created a new resource. Include Location header with the new resource URL.
204No ContentSuccessful DELETE or PUT/PATCH when no response body is needed.
400Bad RequestThe request is malformed — invalid JSON, missing required fields, wrong data types.
401UnauthorizedNo valid authentication credentials provided. The client must authenticate first.
403ForbiddenThe client is authenticated but does not have permission for this action.
404Not FoundThe requested resource does not exist. Also use for hidden resources (instead of 403) for security.
409ConflictThe request conflicts with the current state — duplicate email, version mismatch, etc.
422Unprocessable EntityThe request is well-formed but fails validation — email format invalid, password too short.
429Too Many RequestsRate limit exceeded. Include Retry-After header with seconds until the client can retry.
500Internal Server ErrorAn unexpected server error occurred. Log the details server-side but do not expose them to clients.
503Service UnavailableThe server is temporarily unavailable — maintenance mode, overloaded. Include Retry-After header.

오류 처리

일관된 오류 응답은 API를 더 쉽게 사용할 수 있게 합니다. 모든 오류는 구조화된 JSON 본문을 반환해야 합니다.

표준 오류 응답 형식

모든 오류 응답에 일관된 오류 봉투를 사용하세요.

// Standard error response
{
  "error": {
    "code": "RESOURCE_NOT_FOUND",
    "message": "The user with ID 123 was not found.",
    "status": 404,
    "timestamp": "2026-01-15T10:30:00Z",
    "request_id": "req_abc123def456",
    "documentation_url": "https://api.example.com/docs/errors#RESOURCE_NOT_FOUND"
  }
}

// Another example: authentication error
{
  "error": {
    "code": "TOKEN_EXPIRED",
    "message": "The access token has expired. Please refresh your token.",
    "status": 401,
    "timestamp": "2026-01-15T10:30:00Z",
    "request_id": "req_xyz789"
  }
}

필드 수준 유효성 검사 오류

422 유효성 검사 오류의 경우 필드 수준 오류 정보를 포함하세요.

// 422 Validation error with field-level details
{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "The request body contains invalid fields.",
    "status": 422,
    "details": [
      {
        "field": "email",
        "message": "Must be a valid email address.",
        "rejected_value": "not-an-email"
      },
      {
        "field": "password",
        "message": "Must be at least 8 characters long.",
        "rejected_value": "short"
      },
      {
        "field": "age",
        "message": "Must be a positive integer.",
        "rejected_value": -5
      }
    ],
    "timestamp": "2026-01-15T10:30:00Z",
    "request_id": "req_val456"
  }
}

인증 및 권한 부여

올바른 인증 전략 선택은 사용 사례에 따라 다릅니다. 가장 일반적인 세 가지 접근 방식의 비교입니다.

API 키

헤더나 쿼리 매개변수로 전달되는 간단한 문자열 토큰입니다. 서버 간 통신에 가장 적합합니다.

# API Key in header (recommended)
GET /api/users HTTP/1.1
Host: api.example.com
X-API-Key: sk_live_abc123def456ghi789

# API Key in query parameter (less secure - visible in logs)
GET /api/users?api_key=sk_live_abc123def456ghi789

JWT Bearer 토큰

사용자 클레임을 포함하는 자체 포함 토큰입니다. 마이크로서비스에서의 무상태 인증에 이상적입니다.

# JWT Bearer Token in Authorization header
GET /api/users/me HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...

# Token refresh flow
POST /api/auth/refresh HTTP/1.1
Content-Type: application/json
{
  "refresh_token": "rt_abc123def456"
}

# Response
{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "rt_new789xyz"
}

OAuth 2.0

위임된 권한 부여의 업계 표준입니다. 서드파티 애플리케이션이 사용자를 대신하여 리소스에 접근할 수 있게 합니다.

# OAuth 2.0 Authorization Code Flow

# Step 1: Redirect user to authorization server
GET https://auth.example.com/authorize?
  response_type=code&
  client_id=your_client_id&
  redirect_uri=https://yourapp.com/callback&
  scope=read:users write:users&
  state=random_csrf_token

# Step 2: Exchange authorization code for tokens
POST https://auth.example.com/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code=AUTH_CODE_FROM_CALLBACK&
client_id=your_client_id&
client_secret=your_client_secret&
redirect_uri=https://yourapp.com/callback

# Step 3: Use the access token
GET /api/users/me HTTP/1.1
Authorization: Bearer ACCESS_TOKEN_HERE

인증 방식 비교

기능API KeyJWTOAuth 2.0
ComplexityLowMediumHigh
StatelessYesYesDepends
User contextNoYesYes
Token expirationManual rotationBuilt-in (exp claim)Built-in
Scope/permissionsLimitedVia claimsFine-grained scopes
Third-party accessNoNoYes (delegated)
Best forServer-to-serverSPAs, Mobile appsThird-party integrations
RevocationDelete keyBlacklist neededRevoke at auth server

페이지네이션 패턴

리소스 목록을 반환하는 모든 엔드포인트는 페이지네이션을 지원해야 합니다.

오프셋 기반 페이지네이션

가장 간단한 접근 방식입니다. 페이지 번호와 제한 매개변수를 사용합니다.

# Offset-based pagination request
GET /api/users?page=2&limit=20

# How it works internally
SELECT * FROM users
ORDER BY created_at DESC
LIMIT 20 OFFSET 20;  -- Skip first 20, return next 20

# Pros: Simple, allows jumping to any page
# Cons: Inconsistent with concurrent modifications,
#        slow on large offsets (OFFSET 100000)

커서 기반 페이지네이션

불투명한 커서 토큰을 사용하여 데이터셋 내 위치를 표시합니다. 실시간 데이터에 더 적합합니다.

# Cursor-based pagination request
GET /api/users?cursor=eyJpZCI6MTIzfQ&limit=20

# The cursor is an opaque token (often base64-encoded)
# that points to a specific position in the dataset

# How it works internally (cursor decodes to {"id": 123})
SELECT * FROM users
WHERE id > 123
ORDER BY id ASC
LIMIT 20;

# Pros: Consistent results, fast on large datasets
# Cons: Cannot jump to arbitrary pages,
#        only next/previous navigation

페이지네이션 응답 형식

모든 페이지네이션 응답에 메타데이터를 포함하세요.

// Offset-based pagination response
{
  "data": [
    { "id": 21, "name": "Alice" },
    { "id": 22, "name": "Bob" }
    // ... 18 more items
  ],
  "meta": {
    "current_page": 2,
    "per_page": 20,
    "total_pages": 15,
    "total_count": 294
  },
  "links": {
    "self": "/api/users?page=2&limit=20",
    "first": "/api/users?page=1&limit=20",
    "prev": "/api/users?page=1&limit=20",
    "next": "/api/users?page=3&limit=20",
    "last": "/api/users?page=15&limit=20"
  }
}

// Cursor-based pagination response
{
  "data": [
    { "id": 124, "name": "Charlie" },
    { "id": 125, "name": "Diana" }
    // ... 18 more items
  ],
  "meta": {
    "has_more": true,
    "next_cursor": "eyJpZCI6MTQzfQ",
    "prev_cursor": "eyJpZCI6MTI0fQ"
  },
  "links": {
    "next": "/api/users?cursor=eyJpZCI6MTQzfQ&limit=20",
    "prev": "/api/users?cursor=eyJpZCI6MTI0fQ&limit=20&direction=prev"
  }
}

API 버전 관리 전략

API는 시간이 지남에 따라 진화합니다. 버전 관리를 통해 기존 클라이언트를 중단하지 않고 변경할 수 있습니다.

전략예시장점단점
URL Path/v1/users, /v2/usersExplicit, easy to route and cache, clear in logs and docsURL pollution, harder to sunset old versions
Custom HeaderAccept-Version: v2 or X-API-Version: 2Clean URLs, version hidden from casual usersHarder to test (need tools to set headers), not cacheable by default
Query Parameter/users?version=2Easy to test in browser, no URL path changesCan be accidentally omitted, harder to route, pollutes query string
Content NegotiationAccept: application/vnd.api.v2+jsonMost RESTful approach, per-resource versioningComplex, hard to test, poorly supported by many tools
# URL Path versioning (recommended)
GET /v1/users/123        # Version 1
GET /v2/users/123        # Version 2 with breaking changes

# Express.js router example
const v1Router = express.Router();
const v2Router = express.Router();

v1Router.get('/users/:id', v1UserController.get);
v2Router.get('/users/:id', v2UserController.get);

app.use('/v1', v1Router);
app.use('/v2', v2Router);

속도 제한

속도 제한은 API를 남용으로부터 보호하고 모든 클라이언트의 공정한 사용을 보장합니다.

속도 제한 헤더

클라이언트가 사용량을 모니터링할 수 있도록 모든 응답에 이 헤더를 포함하세요.

HTTP/1.1 200 OK
Content-Type: application/json
X-RateLimit-Limit: 1000          # Max requests per window
X-RateLimit-Remaining: 847       # Remaining requests in window
X-RateLimit-Reset: 1706176800    # Unix timestamp when window resets
X-RateLimit-Window: 3600         # Window duration in seconds (1 hour)

# Some APIs also include:
RateLimit-Policy: 1000;w=3600    # IETF draft standard format

429 요청 과다 응답

클라이언트가 속도 제한을 초과하면 Retry-After 헤더와 함께 429 상태를 반환합니다.

HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 45
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1706176800

{
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "You have exceeded the rate limit of 1000 requests per hour.",
    "status": 429,
    "retry_after": 45,
    "documentation_url": "https://api.example.com/docs/rate-limiting"
  }
}

백오프 전략

클라이언트는 429 응답을 받을 때 지수 백오프를 구현해야 합니다.

// Exponential backoff with jitter
async function fetchWithRetry(url, options, maxRetries = 5) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const response = await fetch(url, options);

    if (response.status !== 429) {
      return response;
    }

    // Use Retry-After header if available
    const retryAfter = response.headers.get('Retry-After');
    let delay;

    if (retryAfter) {
      delay = parseInt(retryAfter, 10) * 1000;
    } else {
      // Exponential backoff: 1s, 2s, 4s, 8s, 16s
      const baseDelay = Math.pow(2, attempt) * 1000;
      // Add jitter (random 0-1s) to prevent thundering herd
      const jitter = Math.random() * 1000;
      delay = baseDelay + jitter;
    }

    console.log(`Rate limited. Retrying in ${delay}ms (attempt ${attempt + 1})`);
    await new Promise(resolve => setTimeout(resolve, delay));
  }

  throw new Error('Max retries exceeded');
}

보안 헤더

보안 헤더는 일반적인 웹 취약점으로부터 API를 보호합니다.

헤더권장 값목적
Access-Control-Allow-Originhttps://yourapp.com (specific origin)CORS: Controls which domains can call your API. Never use * in production with credentials.
Access-Control-Allow-MethodsGET, POST, PUT, PATCH, DELETECORS: Specifies which HTTP methods are allowed for cross-origin requests.
Access-Control-Allow-HeadersAuthorization, Content-TypeCORS: Specifies which request headers are allowed in cross-origin requests.
Strict-Transport-Securitymax-age=31536000; includeSubDomainsHSTS: Forces browsers to use HTTPS for all future requests to your domain.
Content-Security-Policydefault-src 'none'; frame-ancestors 'none'CSP: Prevents XSS and data injection attacks. For APIs, restrict everything.
X-Content-Type-OptionsnosniffPrevents browsers from MIME-type sniffing. Ensures responses are treated as their declared content type.
X-Frame-OptionsDENYPrevents your API responses from being embedded in iframes (clickjacking protection).
Cache-Controlno-store, no-cache, must-revalidatePrevents caching of sensitive API responses. Adjust per endpoint as needed.
// Express.js security headers middleware
const helmet = require('helmet');

app.use(helmet());

// Custom CORS configuration
const cors = require('cors');
app.use(cors({
  origin: ['https://yourapp.com', 'https://admin.yourapp.com'],
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
  allowedHeaders: ['Authorization', 'Content-Type'],
  credentials: true,
  maxAge: 86400  // Cache preflight for 24 hours
}));

// Additional security headers
app.use((req, res, next) => {
  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.setHeader('X-Frame-Options', 'DENY');
  res.setHeader('Cache-Control', 'no-store');
  next();
});

OpenAPI로 API 문서화

좋은 문서는 API 채택에 필수적입니다. OpenAPI는 REST API를 설명하는 표준입니다.

  • 먼저 OpenAPI 사양을 작성한 다음(설계 우선 접근), 사양에 맞게 API를 구현합니다.
  • 모든 엔드포인트, 매개변수 및 응답 스키마에 상세한 설명을 포함하세요.
  • 모든 요청 및 응답 본문에 현실적인 예시 값을 제공하세요.
  • 성공 사례뿐만 아니라 모든 엔드포인트의 오류 응답을 문서화하세요.
  • 태그를 사용하여 관련 엔드포인트를 그룹화하세요.
  • OpenAPI 사양을 API 코드와 함께 버전 관리하세요.
  • 개발자가 직접 요청을 시도할 수 있도록 대화형 문서(Swagger UI 또는 Redoc)를 설정하세요.
# OpenAPI 3.1 specification example
openapi: 3.1.0
info:
  title: User Management API
  version: 1.0.0
  description: API for managing users and their resources

servers:
  - url: https://api.example.com/v1

paths:
  /users:
    get:
      summary: List all users
      tags: [Users]
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
        - name: limit
          in: query
          schema:
            type: integer
            default: 20
            maximum: 100
      responses:
        '200':
          description: A paginated list of users
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserListResponse'
              example:
                data:
                  - id: 1
                    name: "Alice Johnson"
                    email: "alice@example.com"
                meta:
                  current_page: 1
                  total_pages: 10
        '401':
          $ref: '#/components/responses/Unauthorized'

    post:
      summary: Create a new user
      tags: [Users]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateUserRequest'
            example:
              name: "Bob Smith"
              email: "bob@example.com"
              password: "securePassword123"
      responses:
        '201':
          description: User created successfully
        '422':
          $ref: '#/components/responses/ValidationError'

일반적인 REST API 실수

경험 많은 개발자도 이러한 실수를 합니다. API가 모범 사례를 따르는지 확인하세요.

#실수왜 문제인가해결 방법
1Using verbs in URLsREST is resource-oriented; HTTP methods express actionsUse nouns: /users not /getUsers
2Returning 200 for errorsClients rely on status codes for error handlingUse proper status codes: 400, 404, 422, 500
3Not paginating list endpointsReturns grow unbounded, causing timeouts and memory issuesAlways paginate with sensible defaults (limit=20)
4Exposing internal errorsStack traces reveal implementation details to attackersLog details server-side, return generic error messages
5Ignoring CORS headersBrowser-based clients cannot call your APIConfigure CORS for allowed origins and methods
6Not versioning the APIBreaking changes affect all clients simultaneouslyUse URL path versioning: /v1/users
7Using POST for everythingLoses HTTP method semantics, breaks caching and idempotencyUse GET, POST, PUT, PATCH, DELETE appropriately
8Inconsistent naming conventionsMixed case or formats confuse API consumersPick one (snake_case or camelCase) and be consistent
9No rate limitingAPI is vulnerable to abuse and DDoS attacksImplement rate limiting with clear headers
10Missing Content-Type headerClients cannot parse responses correctlyAlways set Content-Type: application/json

관련 개발자 도구 사용해보기

자주 묻는 질문

REST와 RESTful의 차이점은 무엇인가요?

REST는 Roy Fielding이 정의한 아키텍처 스타일입니다. RESTful은 REST 제약을 준수하는 API를 설명하는 형용사입니다. 실제로 대부분의 "REST API"는 모든 REST 제약을 엄격히 따르지 않지만 용어는 호환적으로 사용됩니다.

리소스 업데이트에 PUT과 PATCH 중 어떤 것을 사용해야 하나요?

클라이언트가 리소스의 전체 교체를 보내는 경우 PUT을 사용합니다. 변경된 필드만 보내는 부분 업데이트에는 PATCH를 사용합니다.

API 버전 관리는 어떻게 처리해야 하나요?

URL 경로 버전 관리(예: /v1/users)가 가장 일반적이고 명시적인 접근 방식입니다. 이해, 라우팅, 캐싱이 쉽습니다.

REST API에서 인증을 처리하는 가장 좋은 방법은 무엇인가요?

대부분의 애플리케이션에서 JWT Bearer 토큰은 보안과 단순성의 좋은 균형을 제공합니다. 단기 액세스 토큰과 리프레시 토큰 순환을 사용하세요.

REST API에서 중첩된 리소스를 어떻게 설계해야 하나요?

중첩을 사용하여 부모-자식 관계를 보여줍니다: /users/123/orders. 중첩은 최대 2단계까지로 제한하세요.

REST API 응답에 null 필드를 포함해야 하나요, 생략해야 하나요?

계약에 따라 다릅니다. null 필드를 포함하면 스키마가 예측 가능해집니다. 생략하면 페이로드 크기가 줄어듭니다. 일관성을 유지하고 문서화하세요.

𝕏 Twitterin LinkedIn
도움이 되었나요?

최신 소식 받기

주간 개발 팁과 새 도구 알림을 받으세요.

스팸 없음. 언제든 구독 해지 가능.

Try These Related Tools

4xxHTTP Status Code Reference>>cURL to Code ConverterJWTJWT Decoder{ }JSON Formatter

Related Articles

HTTP 상태 코드: 개발자를 위한 완전 참조 가이드

완전한 HTTP 상태 코드 참조: 1xx~5xx 실용적 설명, API 모범 사례, 디버깅 팁.

API 인증: OAuth 2.0 vs JWT vs API Key

API 인증 방법 비교: OAuth 2.0, JWT Bearer 토큰, API Key. 각 방법의 사용 시나리오, 보안 절충안, 구현 패턴.