REST API 설계 모범 사례: 개발자가 사랑하는 API 만들기

· 12분 읽기

📑 목차

잘 설계된 API는 사용하기 즐겁습니다. 잘못 설계된 API는 좌절감, 버그, 그리고 팀의 리소스를 소진시키는 지원 티켓을 만들어냅니다.

API가 현대 소프트웨어 아키텍처의 중추가 되면서 — 마이크로서비스, 모바일 앱, 서드파티 통합, AI 에이전트를 연결하면서 — 설계를 올바르게 하는 것이 그 어느 때보다 중요해졌습니다. 성공적인 API와 개발자들이 포기하는 API의 차이는 단순히 기능에 관한 것이 아닙니다. 예측 가능성, 일관성, 그리고 개발자 경험에 관한 것입니다.

이 종합 가이드는 훌륭한 API를 평범한 API와 구분하는 실무 사례들을 다루며, 오늘 바로 구현할 수 있는 실제 사례와 실행 가능한 조언을 제공합니다.

REST API 기초

REST(Representational State Transfer)는 엄격한 프로토콜이 아닌 아키텍처 스타일입니다. 핵심 원칙을 이해하면 API 개발 프로세스 전반에 걸쳐 더 나은 설계 결정을 내릴 수 있습니다.

REST 아키텍처의 6가지 기본 제약 조건은 다음과 같습니다:

실제로 REST API는 HTTP 메서드를 의미론적으로 사용합니다: GET은 데이터를 검색하고, POST는 리소스를 생성하며, PUT은 전체 리소스를 대체하고, PATCH는 부분적으로 업데이트하며, DELETE는 리소스를 제거합니다. URL은 동사가 아닌 명사로 리소스를 나타냅니다.

프로 팁: 무상태성은 종종 유지하기 가장 어려운 원칙입니다. 서버에 세션 데이터를 저장하지 마세요. 대신 필요한 모든 인증 및 권한 정보를 포함하는 토큰(예: JWT)을 사용하세요.

URL 설계: 리소스와 네이밍

URL 구조는 개발자가 API를 탐색할 때 가장 먼저 접하는 것입니다. 직관적이고 예측 가능한 URL은 인지 부하를 줄이고 API를 배우고 기억하기 쉽게 만듭니다.

리소스 지향 설계

API를 동작(동사)이 아닌 리소스(명사)를 노출하는 것으로 생각하세요. HTTP 메서드가 동작을 나타내므로 URL은 작업 대상만 식별해야 합니다.

좋음 ✅ 나쁨 ❌ 이유
GET /users GET /getUsers HTTP 메서드가 이미 "get"을 의미함
GET /users/123 GET /user?id=123 리소스 식별자는 경로에 속함
POST /users POST /createUser HTTP 메서드가 "create"를 의미함
DELETE /users/123 POST /deleteUser/123 적절한 HTTP 메서드 사용
GET /users/123/orders GET /getUserOrders?userId=123 계층적 관계가 더 명확함

네이밍 규칙

네이밍의 일관성은 혼란을 방지하고 오류를 줄입니다. 다음 규칙을 따르세요:

비리소스 작업 처리

때로는 리소스 모델에 깔끔하게 맞지 않는 작업을 노출해야 합니다. 이러한 경우 작업 자체를 리소스로 취급하세요:

POST /users/123/password-reset
POST /orders/456/cancellation
POST /reports/generate
GET /search?q=laptop&category=electronics

이러한 엔드포인트는 동작이나 프로세스를 나타내며, 대안이 어색한 리소스 매핑을 강제하는 것일 때 허용됩니다.

빠른 팁: URL을 설계할 때 새로운 개발자에게 설명한다고 상상해보세요. URL이 특정 방식으로 구조화된 이유를 설명하는 데 한 문장 이상이 필요하다면 아마도 너무 복잡한 것입니다.

HTTP 메서드와 상태 코드

HTTP는 작업을 설명하기 위한 풍부한 어휘를 제공합니다. 메서드와 상태 코드를 올바르게 사용하면 API를 예측 가능하게 만들고 캐싱, 디버깅, 표준 HTTP 도구와의 통합이 더 쉬워집니다.

HTTP 메서드

메서드 동작 성공 코드 멱등성 안전성
GET 리소스 검색 200 OK
POST 새 리소스 생성 201 Created 아니오 아니오
PUT 전체 리소스 대체 200 OK / 204 No Content 아니오
PATCH 부분 업데이트 200 OK 아니오* 아니오
DELETE 리소스 제거 204 No Content 아니오
HEAD 헤더만 가져오기 200 OK
OPTIONS 허용된 메서드 가져오기 200 OK

*PATCH는 멱등성을 가지도록 설계할 수 있지만 사양에서 보장되지는 않음

멱등성 이해하기

멱등 작업은 몇 번을 실행하든 동일한 결과를 생성합니다. 이 속성은 네트워크 장애로 인해 재시도가 발생할 수 있는 분산 시스템의 신뢰성에 중요합니다.

GET /users/123은 멱등성이 있습니다 — 한 번 호출하든 100번 호출하든 동일한 사용자 데이터를 반환합니다. DELETE /users/123도 멱등성이 있습니다 — 첫 번째 호출은 사용자를 삭제하고, 후속 호출은 404를 반환하지만 최종 상태는 동일합니다.

POST /users는 멱등성이 없습니다 — 각 호출은 새 사용자를 생성합니다. 멱등 생성이 필요한 경우 클라이언트 생성 ID와 함께 PUT을 사용하거나 멱등성 키를 구현하세요.

필수 상태 코드

200과 500만 사용하지 마세요. 적절한 상태 코드는 클라이언트가 응답 본문을 파싱하지 않고도 응답을 올바르게 처리하는 데 도움이 됩니다.

성공 코드 (2xx):

클라이언트 오류 코드 (4xx):

서버 오류 코드 (5xx):

프로 팁: 429 및 503 응답에는 항상 Retry-After 헤더를 포함하여 클라이언트가 언제 재시도할 수 있는지 알려주세요. 이렇게 하면 서비스가 복구될 때 썬더링 허드 문제를 방지할 수 있습니다.

오류 응답 설계

오류 응답은 많은 API가 부족한 부분입니다. 불명확한 오류 메시지는 5분 수정을 몇 시간의 디버깅으로 바꿀 수 있습니다. 오류 응답은 일관되고, 유익하며, 실행 가능해야 합니다.

표준 오류 형식

모든 오류 응답에서 일관된 구조를 사용하세요:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "요청에 잘못된 데이터가 포함되어 있습니다",
    "details": [
      {
        "field": "email",
        "message": "이메일 주소가 이미 등록되어 있습니다",
        "code": "DUPLICATE_EMAIL"
      },
      {
        "field": "password",
        "message": "비밀번호는 최소 8자 이상이어야 합니다",
        "code": "PASSWORD_TOO_SHORT"
      }
    ],
    "request_id": "req_7f8a9b2c3d4e5f6g",
    "documentation_url": "https://api.example.com/docs/errors/validation"
  }
}

오류 응답 구성 요소

유효성 검사 오류 모범 사례

발견된 첫 번째 오류만이 아니라 모든 유효성 검사 오류를 한 번에 반환하세요. 개발자가 한 오류를 수정한 후 다른 오류를 발견하는 두더지 잡기 게임을 하지 않아야 합니다.

POST /users
{
  "email": "invalid-email",
  "password": "123",
  "age": -5
}

응답: 422 Unprocessable Entity
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "요청 유효성 검사 실패",
    "details": [
      {
        "field": "email",
        "message": "유효한 이메일 주소여야 합니다",
        "code": "INVALID_FORMAT"
      },
      {
        "field": "password",
        "message": "최소 8자 이상이어야 합니다",
        "code": "TOO_SHORT"
      },
      {
        "field": "age",
        "message": "양수여야 합니다",
        "code": "INVALID_VALUE"
      }
    ]
  }
}

보안 고려 사항

오류 메시지에서 민감한 정보가 유출되지 않도록 주의하세요. 사용자 계정이 존재하는지 여부를 밝히거나, 내부 시스템 세부 정보를 노출하거나, 프로덕션에서 스택 추적을 제공하지 마세요.

대신: "사용자 [email protected]을 찾을 수 없습니다"
사용: "잘못된 이메일 또는 비밀번호"

서버 측에서 자세한 오류 정보를 로깅하되 클라이언트에는 정제된 메시지를 반환하세요.

페이지네이션과 필터링

단일 응답으로 수천 개의 레코드를 반환하면 성능이 저하되고 사용자 경험이 나빠집니다. 페이지네이션은 컬렉션을 반환하는 모든 엔드포인트에 필수적입니다.

페이지네이션 전략

오프셋 기반 페이지네이션은 간단하고 익숙합니다:

GET /users?limit=20&offset=40

응답:
{
  "data": [...],
  "pagination": {
    "limit": 20,
    "offset": 40,
    "total": 1247,
    "has_more": true
  }
}

장점: 구현이 쉽고, 임의의 페이지로 이동 지원
단점: 큰 오프셋에서 성능 저하, 요청 간 데이터 변경 시 일관성 없는 결과

커서 기반 페이지네이션은 대규모 데이터셋에 더 견고합니다:

GET /users?limit=20&cursor=eyJpZCI6MTIzNDU2fQ

응답:
{
  "data": [...],
  "pagination": {
    "next_cursor": "eyJpZCI6MTIzNDc2fQ",
    "has_more": true
  }
}

장점: 일관된 결과, 더 나은 성능, 실시간 데이터 처리
단점: 임의의 페이지로 이동 불가, 구현이 약간 더 복잡

페이지 기반 페이지네이션은 UI에 사용자 친화적입니다:

GET /users?page=3&per_page=20

응답:
{
  "data": [...],
  "pagination": {
    "page": 3,
    "per_page": 20,
    "total_pages": 63,
    "total_items": 1247
  }
}

필터링과 정렬

클라이언트가 쿼리 매개변수를 사용하여 결과를 필터링하고 정렬할 수 있도록 허용하세요:

GET /users?status=active&role=admin&sort=-created_at,name

일반적인 패턴: