좋은 REST API는 문서를 깊이 읽지 않아도 다음에 무엇을 할지 짐작되는 예측 가능성을 목표로 합니다. 이는 HTTP의 어휘(메서드·상태 코드·헤더)를 일관되게 활용하고, 리소스를 명확한 규칙으로 표현하는 데서 나옵니다. 이 글은 실무에서 반복적으로 결정해야 하는 설계 축들을 하나씩 짚습니다.
리소스 이름 짓기
REST의 핵심은 '동작'이 아니라 '리소스(명사)'를 URL로 표현하고, 그 리소스에 어떤 동작을 할지는 HTTP 메서드로 나타내는 것입니다. GET /getUsers나 POST /createUser 같은 동사형 경로는 안티패턴입니다.
- 명사·복수형 컬렉션:
/users,/orders. 개별 항목은/users/42. - 동작은 메서드로: 목록은
GET /users, 생성은POST /users, 전체 교체는PUT /users/42, 부분 수정은PATCH /users/42, 삭제는DELETE /users/42. - 계층은 중첩으로:
/users/42/orders(그 사용자의 주문). 단, 3단계를 넘는 깊은 중첩은 피하고 필요하면 최상위 리소스+필터로 대체. - 소문자·하이픈:
/user-profiles(언더스코어나 대문자보다 URL 관례에 맞음). - 일관성: 복수형/단수형, 케이스 규칙을 API 전체에서 한 번 정하고 지킴.
버저닝 전략
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를 스스로 설명하게 만드는 실용적 가치가 큽니다.