REST API — 제약 조건과 RESTful 설계의 의미
"우리 API는 RESTful한가?"라는 질문에 대부분 URL을 근거로 답한다. /getUserInfo?id=123 대신 /users/123을 쓰고 있으니 RESTful하다는 식이다.
그런데 이 근거는 이상하다. URL이 예쁜 것 말고 실제로 무엇이 달라졌는지를 말해주지 않기 때문이다. 주소를 어떻게 짓든 서버가 하는 일은 같고, 클라이언트가 받는 JSON도 같다. 그렇다면 무엇을 보고 RESTful하다고 말하는 것인가.
이 글의 축은 두 문장이다.
REST는 URL 작명 규칙이 아니라 제약 조건의 집합이다.
그리고 그 제약들은 전부 한 방향을 가리킨다 — 요청과 응답을 주고받는 두 당사자 바깥의 누구라도 이해할 수 있게 만드는 것.
앞 글에서 메서드를 "중간 참여자들에게 하는 약속"이라고 정리했다. REST는 그 약속의 전체 체계다. 메서드 하나가 아니라 URI·메서드·상태 코드·헤더·응답 형식 전부를 그 목적에 맞춰 정렬한 것이 REST다.
순서는 이렇다 — 제약이 무엇인지 보고(정의) → 지켰을 때 무엇을 얻는지 두 설계를 대비해 확인하고(이유) → 어디까지 지킬 것인지 판단한다.
1. 정의 — REST는 규칙이 아니라 제약의 집합이다
1-1. 오해 정정 ① — URL 작명 규칙이 아니다
REST를 처음 배울 때 대개 이런 것들을 규칙으로 익힌다.
- 동사 대신 명사를 쓴다 (
/getUsers✕ →/users○) - 복수형을 쓴다
- 계층은 슬래시로 표현한다 (
/users/123/orders) - 소문자와 하이픈을 쓴다
전부 유용한 관례지만, 이것들은 REST가 아니다. REST는 이런 표기 규칙을 하나도 정하지 않았다. /user/123이든 /users/123이든 REST 관점에서는 아무 차이가 없다. 둘 다 리소스를 URI로 식별하고 있으니 제약을 지킨 것이다.
REST는 하나의 아키텍처 스타일이다. 즉 "이렇게 지으세요"가 아니라 **"이런 제약을 지키면 이런 성질을 얻습니다"**라는 형태의 이야기다. 그래서 REST를 이해한다는 것은 규칙 목록을 외우는 게 아니라, 각 제약이 무엇을 얻으려고 무엇을 포기하는지를 아는 것이다.
1-2. 여섯 가지 제약
| 제약 | 내용 | 얻는 것 |
|---|---|---|
| Client-Server | 클라이언트와 서버의 관심사를 분리한다 | 양쪽을 독립적으로 발전시킬 수 있다 |
| Stateless | 서버는 요청 사이의 맥락을 유지하지 않는다 | 서버가 교체 가능한 부품이 된다 |
| Cacheable | 응답은 캐시 가능 여부를 스스로 밝힌다 | 중간 어디서든 재사용할 수 있다 |
| Uniform Interface | 모든 리소스를 같은 방식으로 다룬다 | 인터페이스를 미리 알지 않아도 다룰 수 있다 |
| Layered System | 클라이언트는 다음 계층 너머를 알 수 없다 | 중간에 계층을 자유롭게 끼워 넣을 수 있다 |
| Code on Demand (선택) | 서버가 실행 코드를 내려보낼 수 있다 | 클라이언트 기능을 나중에 확장한다 |
여섯 중 넷은 이미 다룬 주제들이다.
- Stateless — HTTP의 특징 — 비연결성과 무상태에서 다뤘다. 서버 인스턴스를 교체 가능하게 만드는 그 성질이다
- Cacheable — 응답이 캐시 가능한지를 스스로 밝힌다. 헤더로 어떻게 표현하는지는 브라우저 캐싱에 있다
- Layered System — 직전 글 웹 서버와 WAS의 내용 전체가 이 제약의 실제 모습이다. 클라이언트는 자기가 nginx와 이야기하는지 앱과 이야기하는지 알 필요가 없고, 알 수 없어야 한다. 그래야 중간에 CDN·로드밸런서·게이트웨이를 마음대로 끼워 넣을 수 있다
- Code on Demand — 유일한 선택 제약이다. 웹 페이지가 JavaScript를 내려보내는 것이 이 사례다
나머지 하나, Uniform Interface가 REST의 핵심이자 이 글의 본론이다.
1-3. Uniform Interface — 네 개의 하위 제약
"모든 리소스를 같은 방식으로 다룬다"는 말이 추상적이므로, 네 가지로 나눠 본다.
① 리소스를 URI로 식별한다
다루는 대상 하나하나에 주소를 준다. /orders/1024는 1024번 주문을 가리키고, 그 주문에 대한 모든 요청은 이 주소로 간다.
② 표현을 통해 리소스를 조작한다
클라이언트가 받는 JSON은 리소스 그 자체가 아니라 표현이다. 같은 주문을 JSON으로도 XML로도 받을 수 있고, 클라이언트는 받은 표현을 고쳐서 돌려보내는 방식으로 리소스를 수정한다.
③ 자기 서술적 메시지
이것이 이 글에서 가장 중요한 하위 제약이다. 메시지 하나만 보면 그것을 어떻게 처리해야 할지 알 수 있어야 한다는 뜻이다.
GET /orders/1024 HTTP/1.1
Host: api.example.com
Accept: application/json
이 메시지는 바깥의 누구에게도 앞뒤 맥락을 요구하지 않는다. 메서드가 GET이니 안전하고 캐시할 수 있으며, 대상은 /orders/1024이고, 원하는 형식은 JSON이다. 중간의 프록시도, 게이트웨이도, 로그 수집기도 이 한 줄만 보고 자기 일을 판단할 수 있다.
④ HATEOAS
응답이 "다음에 무엇을 할 수 있는지"를 함께 알려준다. 이건 3장에서 길게 다룬다.
여기서 앞 글들과의 관계가 정리된다. HTTP 메서드와 HTTP 상태 코드는 각각 요청과 응답 쪽에서 자기 서술성을 담당하는 장치다. 그리고 REST는 그 장치들을 일관되게 쓰기로 한 약속의 체계다.
1-4. 오해 정정 ② — RESTful의 근거는 URL 모양이 아니다
이제 처음의 질문에 답할 수 있다. "우리 API는 RESTful한가?"의 판단 근거는 URL이 얼마나 예쁜지가 아니다.
바깥에서 이 요청 하나만 보고 무엇을 하려는 요청인지 알 수 있는가.
POST /api라는 요청은 이 질문에 답하지 못한다. 무엇을 하려는지가 바디 안에 들어 있기 때문이다. GET /orders/1024는 답한다. 주소가 예뻐서가 아니라, 메시지 자신이 자기를 설명하고 있어서다.
이 차이가 실제로 무엇을 만들어내는지가 다음 장이다.
2. 이유 — 제약을 지키면 무엇을 얻는가
2-1. 같은 기능, 두 가지 설계
사용자 조회·주문 생성·주문 취소를 제공하는 API를 두 방식으로 만들어보자.
A — 리소스와 메서드를 쓰는 설계
GET /users/123 # 사용자 조회
POST /orders # 주문 생성
DELETE /orders/1024 # 주문 취소
B — 단일 엔드포인트 설계
POST /api HTTP/1.1
Content-Type: application/json
{"action":"getUser","userId":123}
POST /api HTTP/1.1
Content-Type: application/json
{"action":"createOrder","item":"keyboard","qty":1}
POST /api HTTP/1.1
Content-Type: application/json
{"action":"cancelOrder","orderId":1024}
B도 완벽하게 동작한다. 서버 코드도 어렵지 않고, 클라이언트도 함수 하나로 감쌀 수 있다. 그러면 A를 택할 이유는 무엇인가.
2-2. B가 잃는 것 — 중간 참여자 전부
B의 문제를 흔히 "URL만 봐서는 뭘 하는지 모른다"로 요약한다. 맞는 말이지만 그건 표면이다. 진짜 문제는 이것이다.
<!-- IMAGE: 위쪽은 모든 요청이 POST /api로 갈 때 CDN·로드밸런서·모니터링·재시도 로직이 각각 아무 판단도 하지 못하는 모습, 아래쪽은 리소스 URI와 메서드를 쓸 때 캐시·경로별 라우팅·엔드포인트별 집계·멱등 재시도가 가능해지는 모습 -->모든 요청이
POST /api라서, 중간에 있는 참여자 전부가 아무 판단도 할 수 없게 된다.
참여자별로 보면 차이가 분명하다.
| 참여자 | A에서 할 수 있는 일 | B에서는 |
|---|---|---|
| CDN·프록시 | GET /users/123을 캐시해 같은 요청을 원서버까지 보내지 않는다 | POST는 캐시 대상이 아니다. 모든 조회가 원서버로 간다 |
| 재시도 로직 | GET·DELETE는 멱등이므로 응답이 없으면 다시 보낸다 | 전부 POST다. 재시도하면 위험하므로 포기한다 |
| 로드밸런서 | 경로별로 다른 백엔드로 보내고, 읽기를 리드 레플리카로 돌린다 | URL이 하나뿐이라 나눌 기준이 없다 |
| 모니터링 | 엔드포인트별 응답 시간·에러율을 본다. 어디가 느린지 보인다 | POST /api 하나로 뭉뚱그려진다. 평균만 남는다 |
| 접근 제어 | 경로와 메서드로 권한을 건다 (DELETE /orders/*는 관리자만) | 바디를 파싱해야 무슨 요청인지 알 수 있다 |
| 브라우저 | 뒤로 가기·북마크·새로고침이 자연스럽게 동작한다 | 전부 POST라 북마크도 뒤로 가기도 성립하지 않는다 |
표의 오른쪽 열이 전부 같은 원인에서 나온다. 판단에 필요한 정보가 바디 안에 들어가 있어서, 바디를 읽지 않는 참여자들은 아무것도 알 수 없다는 것.
B는 HTTP를 쓰는 것이 아니라 HTTP를 터널로 쓰는 것이다.
진짜 요청은 바디 안에 숨어 있고, 바깥의 HTTP는 그것을 나르는 봉투 역할만 한다. 그래서 웹 인프라가 수십 년 동안 쌓아온 기능을 통째로 포기하게 된다. 캐시도, 재시도도, 라우팅도, 관측도.
여기서 앞 글과 이어진다. HTTP 메서드 글에서 "메서드는 중간 참여자들에게 하는 약속"이라고 했다. 그 관점에서 보면,
REST는 그 약속의 전체 체계다. 메서드만이 아니라 URI·상태 코드·헤더까지 전부 같은 목적으로 정렬한 것.
2-3. 오해 정정 ③ — 그렇다고 B가 항상 나쁜 것은 아니다
여기서 균형을 잡아야 한다. 위의 이점들은 공짜로 값을 하는 게 아니라 특정 조건에서만 값을 한다.
- CDN 캐시가 의미 있으려면 캐시할 만한 읽기 요청이 많아야 한다
- 브라우저의 북마크·뒤로 가기가 의미 있으려면 클라이언트가 브라우저여야 한다
- 경로별 라우팅이 의미 있으려면 중간에 그런 판단을 하는 계층이 있어야 한다
그러면 이런 조건이 하나도 성립하지 않는 곳은 어떤가. 내부 서비스끼리 통신하는 경우가 그렇다. CDN도 없고 브라우저도 없고, 앞뒤가 서로를 정확히 아는 두 프로그램이 이야기한다. 여기서는 위 표의 이점 대부분이 값을 하지 않는다.
그래서 다른 선택지들이 존재한다. 그쪽 이야기는 다음 글들에서 다룬다.
gRPC — 내부 통신처럼 중간 참여자가 없는 맥락에서 무엇을 대신 얻는가. GraphQL — 클라이언트가 필요한 것을 직접 지정하는 방식과 그 대가.
REST가 항상 옳은 것이 아니라, REST가 값을 하는 조건이 따로 있다. 그 조건을 알아야 언제 다른 것을 골라야 하는지도 판단할 수 있다.
3. 판단 — 어디까지 지킬 것인가
3-1. 성숙도 모델과 HATEOAS
REST를 얼마나 지키고 있는지를 단계로 나눈 모델이 있다. 흔히 Richardson 성숙도 모델이라 부른다.
| 단계 | 상태 | 예 |
|---|---|---|
| Level 0 | 엔드포인트 하나에 전부 밀어 넣는다 | 2-1의 B |
| Level 1 | 리소스를 URI로 분리한다 | POST /users/123, POST /orders/1024 |
| Level 2 | 메서드와 상태 코드를 의미대로 쓴다 | 2-1의 A |
| Level 3 | 응답이 다음 행동을 링크로 알려준다 (HATEOAS) | 아래에서 다룬다 |
그리고 현실은 이렇다.
"REST API"라고 부르는 것 대부분은 Level 2다.
그러면 Level 3은 무엇이고, 왜 아무도 거기까지 가지 않는가. 이 글에서 가장 지면을 많이 줄 부분이다.
HATEOAS — 응답이 다음 행동을 알려준다
개념은 간단하다. 응답에 "지금 이 리소스로 할 수 있는 일 목록"을 함께 담는 것이다. 클라이언트는 진입점 URL 하나만 알고, 그 뒤로는 서버가 준 링크만 따라간다.
낯설게 들리지만, 우리는 이미 매일 이 방식을 쓰고 있다. 웹 페이지가 그렇다.
쇼핑몰에서 주문 내역 페이지를 볼 때, 우리는 /orders/1024/cancel이라는 주소를 외워서 입력하지 않는다. 페이지에 있는 "주문 취소" 링크를 누른다. 그 링크가 있으면 취소할 수 있는 것이고, 없으면 취소할 수 없는 것이다. URL 구조를 알 필요가 없고, 정책이 바뀌어 링크가 사라지면 그냥 그것을 따른다.
HATEOAS는 이 방식을 API에도 적용하자는 것이다.
주문의 생애주기로 보기
같은 GET /orders/1024인데 주문 상태에 따라 응답이 달라진다.
① 결제 대기 (pending)
{
"id": 1024,
"status": "pending",
"total": 39000,
"_links": {
"self": { "href": "/orders/1024" },
"pay": { "href": "/orders/1024/payment", "method": "POST" },
"cancel": { "href": "/orders/1024", "method": "DELETE" }
}
}
② 결제 완료 (paid)
{
"id": 1024,
"status": "paid",
"total": 39000,
"_links": {
"self": { "href": "/orders/1024" },
"refund": { "href": "/orders/1024/refund", "method": "POST" },
"shipping": { "href": "/orders/1024/shipping", "method": "GET" }
}
}
pay와 cancel이 사라지고 refund가 생겼다.
③ 배송 시작 (shipped)
{
"id": 1024,
"status": "shipped",
"total": 39000,
"_links": {
"self": { "href": "/orders/1024" },
"track": { "href": "/orders/1024/tracking", "method": "GET" }
}
}
refund도 사라졌다. "배송이 시작되면 환불할 수 없다"는 정책이 링크의 유무로 표현된 것이다. 문서에만 적혀 있던 규칙이 응답 안으로 들어왔다.
권한도 같은 방식으로 드러난다. 남의 주문을 조회한 사용자에게는 self만 남는다. 볼 수는 있지만 아무것도 할 수 없다는 뜻이 별도의 필드 없이 표현된다.
무엇을 얻으려는 것인가 — 비즈니스 규칙이 어디에 사는가
HATEOAS가 겨냥하는 것은 링크 자체가 아니다. 규칙이 몇 군데에 복제되는가다.
환불 정책이 "결제 후 7일 이내, 기프트카드 제외, 본인만"이라고 하자. HATEOAS가 없으면 이 규칙이 어디에 있어야 하는가.
- 서버 — 실제로 검증해야 하므로 당연히 있다
- 웹 — 환불 버튼을 보여줄지 판단해야 하므로 있어야 한다
- iOS — 같은 판단이 필요하므로 또 있다
- 안드로이드 — 또 있다
같은 규칙이 네 곳에 복제된다. 그리고 정책이 "7일"에서 "14일"로 바뀌면 네 곳을 다 고쳐서 함께 배포해야 한다. 모바일 앱은 심사와 업데이트 배포가 걸리므로 그 사이에는 이런 상태가 된다.
버튼은 보이는데 누르면 실패한다. 또는 누를 수 있는데 버튼이 안 보인다.
여기에 하나 더 있다. 클라이언트가 URL을 알고 있으면 서버가 URL 구조를 바꿀 수 없다. 모든 클라이언트가 /orders/{id}/refund를 하드코딩하고 있으니, 주소를 바꾸는 순간 전부 깨진다.
HATEOAS는 이 둘을 함께 해결하려는 시도다. 규칙은 서버 한 곳에만 살고, 클라이언트는 링크가 있으면 보여주고 없으면 감춘다. URL도 서버가 정하므로 언제든 바꿀 수 있다.
클라이언트 코드로 보면 차이가 분명하다.
HATEOAS가 없을 때 — 규칙이 컴포넌트 안으로 들어온다.
function OrderActions({ order, user }: { order: Order; user: User }) {
const paidAt = new Date(order.paidAt).getTime();
const withinRefundPeriod = Date.now() - paidAt < 7 * 24 * 60 * 60 * 1000;
return (
<>
{order.status === 'pending' && <PayButton orderId={order.id} />}
{order.status === 'pending' && order.ownerId === user.id && (
<CancelButton orderId={order.id} />
)}
{order.status === 'paid' &&
withinRefundPeriod &&
!order.hasGiftCardItem &&
order.ownerId === user.id && <RefundButton orderId={order.id} />}
{order.status === 'shipped' && <TrackButton orderId={order.id} />}
</>
);
}
이 조건문들이 서버 코드에도 똑같이 있다. 그리고 iOS와 안드로이드에도 있다.
HATEOAS가 있을 때 — 조건문이 전부 사라진다.
const ACTION_LABEL: Record<string, string> = {
pay: '결제하기',
cancel: '주문 취소',
refund: '환불 요청',
track: '배송 조회',
};
function OrderActions({ order }: { order: Order }) {
return (
<>
{Object.entries(order._links)
.filter(([rel]) => rel !== 'self')
.map(([rel, link]) => (
<ActionButton key={rel} href={link.href} method={link.method}>
{ACTION_LABEL[rel]}
</ActionButton>
))}
</>
);
}
status도, 7일도, 기프트카드도, 소유자 확인도 없다. "서버가 준 것을 그린다"만 남는다. 환불 기간이 14일로 바뀌어도 이 코드는 고칠 필요가 없다.
이미 부분적으로 쓰고 있다 — 페이지네이션
HATEOAS가 낯설게 느껴지지만, 사실 우리는 이미 일부를 쓰고 있다. 대표적인 것이 페이지네이션이다.
{
"items": [ /* ... */ ],
"_links": {
"self": { "href": "/orders?page=3&size=20" },
"first": { "href": "/orders?page=1&size=20" },
"prev": { "href": "/orders?page=2&size=20" },
"next": { "href": "/orders?page=4&size=20" }
}
}
마지막 페이지에서는 next가 빠진다. 그러면 클라이언트는 "다음" 버튼을 비활성화한다. totalCount / pageSize를 계산해 현재 페이지와 비교하지 않는다. 이건 정확히 HATEOAS의 발상이다. 계산해서 판단하지 않고 링크의 유무를 따른다.
커서 기반 페이지네이션에서는 이 성격이 더 뚜렷해진다. 다음 커서 값은 서버가 만들어낸 불투명한 문자열이라 클라이언트가 스스로 계산해낼 방법이 아예 없다. 링크를 따라가는 것 외에 선택지가 없는 것이다.
표준 포맷
링크를 담는 방식은 몇 가지가 표준화돼 있다.
| 포맷 | 특징 |
|---|---|
| HAL | 가장 단순하고 널리 쓰인다. 위 예시의 _links가 이 형식이다 |
| JSON:API | 더 엄격하다. relationships와 links의 구조까지 정해져 있어 자유도가 낮은 대신 일관성이 높다 |
Java 진영에는 HAL 형식 응답을 만들어주는 라이브러리가 있어서, 그쪽 코드에서 _links를 상대적으로 자주 보게 된다.
그런데 왜 아무도 완전히 지키지 않는가
여기가 이 글의 균형점이다. 얻는 것이 저렇게 분명한데도 Level 3까지 가는 API는 드물다. 이유가 셋 있다.
① 클라이언트가 사람이 아니다
웹 페이지에서 링크가 잘 작동하는 이유는 사람이 보고 판단하기 때문이다. "주문 취소"라는 글자를 읽고 무슨 뜻인지 이해한다.
프로그램은 그렇지 않다. "refund"라는 문자열을 받아도 그게 무엇인지 모른다. 결국 위의 두 번째 코드에서 본 ACTION_LABEL처럼 rel 이름과 그 의미를 클라이언트에 하드코딩하게 된다. 그러면 되묻게 된다. 결합이 정말 사라졌는가?
절반은 사라졌다. URL과 조건 판단은 서버로 갔지만, "어떤 rel이 존재할 수 있는가"라는 지식은 여전히 클라이언트에 남는다. 서버가 새 rel을 추가해도 클라이언트가 모르면 화면에 나오지 않는다.
② UI는 미리 알아야 한다
화면을 설계하려면 어떤 버튼이 있을 수 있는지 미리 알아야 한다. 버튼이 몇 개까지 나올 수 있는지에 따라 레이아웃이 달라지고, 각 버튼의 스타일과 위치도 정해야 한다. 응답을 받고 나서야 아는 것으로는 화면을 짤 수 없다.
③ 비용이 작지 않다
- 응답이 무거워진다. 데이터보다 링크가 긴 경우도 생긴다
- 서버는 매 응답마다 "이 사용자가 이 리소스로 지금 할 수 있는 일"을 계산해야 한다
- 클라이언트 코드도 마냥 단순해지지는 않는다. 링크를 해석하고 라우팅하는 계층이 새로 필요하다
그래서 이 글이 취하는 입장
두 가지 훈계를 모두 피하고 싶다.
- "Level 2는 가짜 REST다" — 그렇게 말해봐야 아무 판단도 도와주지 않는다. Level 2까지만으로도 2-2에서 본 이점은 전부 얻는다
- "HATEOAS는 이상론이라 쓸모없다" — 그것이 무엇을 해결하려 했는지 모르면, 같은 문제를 매번 다시 만나면서도 알아보지 못한다
전부 아니면 전무가 아니다. 링크로 가능한 행동을 알려준다는 아이디어만 부분적으로 가져다 써도 충분히 실용적이다. 페이지네이션이 이미 그렇게 하고 있고, 상태가 복잡한 리소스에만 _links를 붙이는 것도 방법이다.
그래서 "우리 API는 RESTful한가?"라는 질문에는 이렇게 되물을 수 있다.
무엇을 위해 그러려는가?
캐시와 라우팅과 관측이 필요해서라면 Level 2로 충분하다. 클라이언트 여러 개에 규칙이 복제되는 것이 실제 문제라면 그 부분에만 링크를 도입하면 된다. 목적 없이 단계만 올리는 것은 비용만 남는다.
3-2. 리소스로 표현하기 어려운 것들
REST를 성실하게 적용하려 할 때 반드시 걸리는 지점이 있다. 명백히 행위인 것들이다.
- 로그인
- 검색
- 비밀번호 재설정
- 결제 승인
"모든 것은 리소스"라는 원칙을 밀어붙이면 억지가 나온다. 로그인을 굳이 POST /sessions라고 부르는 것이 대표적이다. 세션을 리소스로 본다는 설명이 아주 틀린 것은 아니지만, 실제로 하려는 일은 인증이지 리소스 생성이 아니다.
현실적인 타협은 두 갈래다.
① 행위를 리소스로 승격시킨다
"비밀번호를 재설정한다"가 아니라 "비밀번호 재설정 요청"이라는 것이 생겼다고 본다.
POST /password-reset-requests
무리한 해석 같지만, 실제로 그 요청은 만료 시간과 상태를 가진 개체다. 조회할 수도 있고, 취소할 수도 있다. 리소스로 볼 만한 실체가 정말 있는 경우에는 이 방향이 자연스럽다.
② 액션 엔드포인트를 허용한다
POST /orders/1024/cancel
POST /orders/1024/payment
리소스 하위에 행위를 두는 방식이다. 순수한 REST는 아니지만 널리 쓰이고, 무엇을 하는지 분명하다. POST /users/1/password-reset 같은 형태가 리소스인지 행위인지 애매한 것도 사실인데, 그 애매함을 인정하고 쓰는 편이 억지 명사를 만드는 것보다 나은 경우가 많다.
여기서 정답을 정하지는 않겠다. 다만 하나는 분명하다.
어느 쪽을 고르든, API 전체에서 일관되게 쓰는 것이 어느 한쪽이 더 옳은 것보다 중요하다.
같은 API 안에서 어떤 것은 POST /sessions이고 어떤 것은 POST /auth/login이면, 쓰는 쪽은 매번 문서를 확인해야 한다. 예측 가능성이 Uniform Interface가 원래 얻으려던 것이라는 점을 생각하면, 일관성이 곧 그 제약의 실천이기도 하다.
3-3. 정리
이 글을 다섯 줄로 압축하면 이렇다.
- REST는 URL 작명 규칙이 아니라 제약 조건의 집합이다. 복수형이냐 단수형이냐는 REST가 정한 것이 아니다.
- 판단 근거는 URL 모양이 아니라 "바깥에서 이 요청 하나만 보고 알 수 있는가"다.
- 제약을 어기면 중간 참여자를 전부 잃는다. 캐시·재시도·라우팅·관측·접근 제어가 한꺼번에 무력해진다.
- 그래도 REST가 항상 옳은 것은 아니다. 중간 참여자가 없는 맥락에서는 그 이점 대부분이 값을 하지 않는다.
- HATEOAS는 규칙이 여러 클라이언트에 복제되는 문제를 겨냥한 것이고, 완전히 지키기 어려운 이유도 분명하다. 부분적으로 쓰는 것으로 충분한 경우가 많다.
처음의 질문에 답하면 이렇게 된다.
REST는 지키면 점수를 받는 규칙이 아니라, 요청과 응답을 두 당사자 바깥에서도 읽을 수 있게 만들려는 제약이다. 그래서 "RESTful한가"보다 먼저 물어야 할 것은 "우리 요청을 읽어야 하는 참여자가 있는가, 있다면 그들이 지금 읽을 수 있는가"다.
관련 글
- HTTP 메서드 — 안전성, 멱등성, 그리고 중간 참여자들과의 약속 — REST가 체계화한 그 약속의 원문.
- HTTP 상태 코드 — 응답 쪽에서 자기 서술성을 담당하는 장치.
- HTTP의 특징 — 비연결성과 무상태 — Stateless 제약이 실제로 무엇을 얻는가.
- 브라우저 캐싱 — Cacheable 제약의 구현.
- 웹 서버와 WAS — Layered System 제약이 실제 배치에서 어떻게 나타나는가.
- gRPC · GraphQL — REST의 이점이 값을 하지 않는 맥락에서의 선택지.