REST API 설계 원칙

리소스 이름 짓기, 버저닝 전략, 커서·오프셋 페이지네이션, 필터·정렬, RFC 9457 problem+json 오류 형식, 그리고 HATEOAS 기초를 다룹니다.

좋은 REST API는 문서를 깊이 읽지 않아도 다음에 무엇을 할지 짐작되는 예측 가능성을 목표로 합니다. 이는 HTTP의 어휘(메서드·상태 코드·헤더)를 일관되게 활용하고, 리소스를 명확한 규칙으로 표현하는 데서 나옵니다. 이 글은 실무에서 반복적으로 결정해야 하는 설계 축들을 하나씩 짚습니다.

리소스 이름 짓기

REST의 핵심은 '동작'이 아니라 '리소스(명사)'를 URL로 표현하고, 그 리소스에 어떤 동작을 할지는 HTTP 메서드로 나타내는 것입니다. GET /getUsersPOST /createUser 같은 동사형 경로는 안티패턴입니다.

버저닝 전략

API는 시간이 지나면 호환성을 깨는 변경이 필요해지므로, 기존 클라이언트를 보호할 버저닝이 필요합니다. 대표적으로 세 가지 방식이 있습니다. URL 경로 버저닝(/v1/users)은 가장 흔하고 눈에 잘 띄며 캐시·라우팅이 쉽습니다. 헤더 버저닝(Accept: application/vnd.example.v2+json)은 URL을 깔끔하게 유지하지만 눈에 덜 띄고 테스트가 번거롭습니다. 쿼리 파라미터 버저닝(/users?version=2)은 간단하지만 캐시 키 관리가 까다롭습니다. 어느 방식이든 핵심 원칙은 하위 호환을 최대한 지키고(필드 추가는 비파괴적), 버전을 올려야 하는 파괴적 변경은 신중히 모아서 하는 것입니다.

페이지네이션: 오프셋 vs 커서

목록이 크면 한 번에 다 주지 말고 나눠 줘야 합니다. 오프셋 페이지네이션(?limit=20&offset=40)은 직관적이고 임의 페이지로 점프하기 쉽지만, 두 가지 약점이 있습니다 — 뒤쪽 페이지일수록 데이터베이스가 앞 행을 세느라 느려지고, 페이지를 넘기는 사이 새 항목이 추가되면 항목이 밀려 중복·누락이 생깁니다. 커서 페이지네이션(?limit=20&cursor=eyJpZCI6NDB9)은 '마지막으로 본 항목의 위치'를 불투명한 커서로 넘겨 그 다음부터 이어받는 방식으로, 대용량에서 성능이 안정적이고 실시간으로 데이터가 바뀌어도 일관됩니다. 대신 임의 페이지 번호로 점프할 수는 없습니다. 무한 스크롤·대규모 데이터에는 커서를, 관리자 표처럼 페이지 번호가 필요한 UI에는 오프셋을 고려하세요.

GET /v1/orders?status=paid&sort=-created_at&limit=20&cursor=eyJpZCI6MTAwfQ HTTP/1.1
Host: api.example.com

HTTP/1.1 200 OK
Content-Type: application/json

{
  "data": [ { "id": 101, "status": "paid" } ],
  "page": { "next_cursor": "eyJpZCI6MTIwfQ", "has_more": true }
}

필터링과 정렬

컬렉션 엔드포인트에는 쿼리 파라미터로 필터·정렬·검색을 얹습니다. 필터는 필드명을 그대로 쓰고(?status=paid&role=admin), 정렬은 관례적으로 필드명 앞에 -를 붙여 내림차순을 표현합니다(?sort=-created_at,name = 생성일 내림차순, 이름 오름차순). 범위 필터는 ?price_gte=1000&price_lte=5000 같은 접미사 규칙이나 별도 문법을 정해 씁니다. 여기서도 중요한 것은 일관성문서화입니다. 규칙을 한 번 정해 모든 컬렉션에서 동일하게 적용하면 클라이언트가 학습 없이 재사용할 수 있습니다.

오류 형식: RFC 9457 problem+json

오류 응답의 형식이 엔드포인트마다 제각각이면 클라이언트의 오류 처리가 지옥이 됩니다. 표준 해법이 RFC 9457(구 RFC 7807, application/problem+json)입니다. 이 형식은 type(오류 종류를 식별하는 URI), title(사람이 읽는 요약), status(HTTP 상태 코드), detail(이번 사례의 구체적 설명), instance(문제가 발생한 리소스)의 표준 필드를 정의하고, 필요하면 확장 필드를 더할 수 있습니다. 상태 코드는 여전히 1차 신호이고, problem+json은 그 위에 기계가 읽을 수 있는 구조화된 상세를 얹습니다.

HTTP/1.1 422 Unprocessable Entity
Content-Type: application/problem+json

{
  "type": "https://api.example.com/errors/validation",
  "title": "Validation failed",
  "status": 422,
  "detail": "email must be a valid address",
  "instance": "/v1/users",
  "errors": [ { "field": "email", "code": "format" } ]
}

HATEOAS 기초

HATEOAS(Hypermedia as the Engine of Application State)는 응답 안에 '다음에 할 수 있는 동작들의 링크'를 함께 담아, 클라이언트가 URL을 하드코딩하지 않고 서버가 준 링크를 따라가게 하는 REST의 원형 이념입니다. 예를 들어 주문 응답에 _links: { "cancel": "/v1/orders/101/cancel", "self": "/v1/orders/101" }를 넣으면, 취소 가능 여부와 그 경로를 서버가 상태에 따라 알려줄 수 있습니다. 완전한 HATEOAS는 실무에서 드물지만, 응답에 관련 리소스 링크(self·next·관련 항목)를 포함하는 부분적 채택은 결합도를 낮추고 API를 스스로 설명하게 만드는 실용적 가치가 큽니다.