REST API 설계 원칙 — 네이밍, 상태 코드, 버전 관리

REST API 설계가 중요한 이유

API는 서비스의 정문입니다. 방문자(클라이언트)가 건물(서버)에 들어올 때 안내 표지판이 명확해야 원하는 곳을 쉽게 찾을 수 있습니다. 잘 설계된 API는 문서를 읽지 않아도 사용법을 추측할 수 있어야 합니다.

REST(Representational State Transfer)는 HTTP 프로토콜을 활용한 아키텍처 스타일입니다. 핵심 원칙은 리소스 중심, HTTP 메서드 활용, **상태 없음(Stateless)**입니다.

URI 네이밍 규칙

기본 원칙

# 잘못된 설계 — 동사 사용, 일관성 없음
GET  /getUsers
POST /createNewUser
GET  /user/delete/123
POST /updateUserProfile

# 올바른 설계 — 명사, 복수형, 계층 구조
GET    /users              # 사용자 목록 조회
POST   /users              # 사용자 생성
GET    /users/123          # 특정 사용자 조회
PUT    /users/123          # 사용자 전체 수정
PATCH  /users/123          # 사용자 부분 수정
DELETE /users/123          # 사용자 삭제

네이밍 체크리스트

규칙올바른 예잘못된 예
복수 명사 사용/users, /orders/user, /order
소문자 사용/user-profiles/UserProfiles
하이픈 사용/blog-posts/blog_posts, /blogPosts
동사 사용 금지POST /users/createUser
파일 확장자 금지/users/123/users/123.json
계층 관계 표현/users/123/orders/getUserOrders?id=123

관계형 리소스

# 사용자의 주문 목록
GET /users/123/orders

# 사용자의 특정 주문
GET /users/123/orders/456

# 주문의 결제 정보
GET /users/123/orders/456/payment

# 깊은 중첩은 피하기 (3단계 이상)
# 잘못됨: /users/123/orders/456/items/789/reviews
# 올바름: /order-items/789/reviews

HTTP 메서드와 상태 코드

메서드별 의미

메서드용도멱등성안전성요청 본문
GET조회없음
POST생성아니오아니오있음
PUT전체 교체아니오있음
PATCH부분 수정아니오아니오있음
DELETE삭제아니오없음/선택

상태 코드 가이드

# 성공 응답
200 OK              # 일반 성공 (GET, PUT, PATCH, DELETE)
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 # 요청 한도 초과 (Rate Limit)

# 서버 에러
500 Internal Error   # 서버 내부 오류
502 Bad Gateway      # 업스트림 서버 오류
503 Service Unavailable # 서비스 일시 중단

에러 응답 패턴

일관된 에러 응답 형식은 클라이언트 개발자의 생산성을 크게 높입니다.

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "입력 데이터가 유효하지 않습니다.",
    "details": [
      {
        "field": "email",
        "message": "유효한 이메일 형식이 아닙니다.",
        "value": "invalid-email"
      },
      {
        "field": "age",
        "message": "나이는 0 이상이어야 합니다.",
        "value": -5
      }
    ],
    "timestamp": "2026-02-19T14:30:00Z",
    "request_id": "req_abc123"
  }
}

Express.js 에러 핸들링 구현

// 에러 응답 헬퍼 함수
class ApiError extends Error {
  constructor(statusCode, code, message, details = []) {
    super(message);
    this.statusCode = statusCode;
    this.code = code;       // 에러 코드 (머신 리더블)
    this.details = details; // 상세 정보 배열
  }
}

// 유효성 검증 에러 생성
function validationError(details) {
  return new ApiError(
    400,
    "VALIDATION_ERROR",
    "입력 데이터가 유효하지 않습니다.",
    details
  );
}

// 글로벌 에러 핸들러
app.use((err, req, res, next) => {
  const statusCode = err.statusCode || 500;
  const response = {
    error: {
      code: err.code || "INTERNAL_ERROR",
      message: err.message || "서버 내부 오류가 발생했습니다.",
      details: err.details || [],
      timestamp: new Date().toISOString(),
      request_id: req.id  // 요청 추적용 ID
    }
  };

  // 500 에러는 상세 내용을 숨김 (보안)
  if (statusCode === 500) {
    console.error("서버 에러:", err);
    response.error.message = "서버 내부 오류가 발생했습니다.";
    response.error.details = [];
  }

  res.status(statusCode).json(response);
});

페이지네이션

# 커서 기반 (권장 — 대규모 데이터에 적합)
GET /users?cursor=eyJpZCI6MTAwfQ&limit=20

# 응답
{
  "data": [...],
  "pagination": {
    "next_cursor": "eyJpZCI6MTIwfQ",
    "has_more": true,
    "limit": 20
  }
}

# 오프셋 기반 (간단하지만 대규모에서 성능 저하)
GET /users?page=3&per_page=20

# 응답
{
  "data": [...],
  "pagination": {
    "page": 3,
    "per_page": 20,
    "total": 150,
    "total_pages": 8
  }
}
방식장점단점
커서 기반대규모 안정적, 실시간 데이터총 페이지 수 모름, 임의 페이지 불가
오프셋 기반구현 간단, 임의 페이지 가능데이터 변경 시 중복/누락, 느림

버전 관리

# 방법 1: URL 경로 (가장 직관적, 권장)
GET /v1/users
GET /v2/users

# 방법 2: 쿼리 파라미터
GET /users?version=2

# 방법 3: 헤더
GET /users
Accept: application/vnd.myapi.v2+json
방식장점단점
URL 경로명확, 캐싱 용이URL 변경
쿼리 파라미터URL 구조 유지기본값 혼란
헤더URL 깔끔테스트 불편, 발견성 낮음

필터링, 정렬, 검색

# 필터링 — 쿼리 파라미터 사용
GET /users?status=active&role=admin

# 정렬 — sort 파라미터
GET /users?sort=-created_at,name
# -는 내림차순, 기본은 오름차순

# 검색 — q 또는 search 파라미터
GET /users?q=김철수

# 필드 선택 — fields 파라미터 (응답 크기 최적화)
GET /users?fields=id,name,email

# 조합 사용
GET /users?status=active&sort=-created_at&fields=id,name&limit=10

실전 팁

  • 일관성이 가장 중요합니다: 네이밍, 응답 형식, 에러 구조를 프로젝트 전체에서 통일하세요. 일관성 없는 API는 문서가 아무리 좋아도 사용하기 어렵습니다.
  • HATEOAS보다 실용성을 선택하세요: 이론적으로 완벽한 REST보다 개발자가 쉽게 사용할 수 있는 API가 좋은 API입니다.
  • Rate Limiting을 초기부터 적용하세요: 429 응답과 Retry-After 헤더를 포함하세요. 나중에 추가하면 기존 클라이언트가 대응하기 어렵습니다.
  • 요청 ID를 모든 응답에 포함하세요: 디버깅 시 클라이언트와 서버 로그를 연결하는 핵심 정보입니다.
  • PUT과 PATCH를 구분하세요: PUT은 전체 교체(빠진 필드는 null), PATCH는 부분 수정(보낸 필드만 변경)입니다. 대부분의 수정 요청에는 PATCH가 적합합니다.
  • OpenAPI(Swagger)로 문서화하세요: 코드에서 API 스펙을 자동 생성하면 문서와 실제 API 간의 불일치를 방지할 수 있습니다.

이 글이 도움이 되었나요?