GitHub REST API — HATEOAS 잔여물 일부 있음 (response 에 URL, pagination Link header), 근데 대부분 client 가 무시하고 어쨌든 URI 하드코딩.
AWS — 무거운 RPC, 서명 request, hypermedia 없음.
Twilio, SendGrid, Slack, Anthropic, Google API — 어느 것도 HATEOAS-driven 아님.
Fielding 의 엄격 정의로 보면 사실상 모두가 인 것은 "URI convention 과 JSON body 가진 HTTP 위 RPC." Dissertation 의미의 REST 아님. 그리고 다 동작, scale, ship.
HATEOAS 가 안 이긴 이유
HATEOAS 가 제공하는 이득 (URI 진화, 동적 capability 발견, generic client) 이 대부분 API 소비자가 못 지불하거나 안 지불하는 비용 필요:
1. Client 가 generic 아님. 브라우저가 어느 HTML 사이트나 걷는 방식으로 어느 JSON API 나 걷는 단일 "HATEOAS client" 의 꿈이 절대 실현 안 됨. 모든 API 에 link 만으로 표현 못 하는 비즈니스 semantics 있어 — error handling, retry 정책, edge case. Link 따라가는데 order 가 뭔지 모르는 generic client 가 실제로 유용한 거 못 함. 그래서 client 가 API-specific 되고, API-specific 되는 순간 매 request 마다 link 걷는 것보다 빨라서 URI template 하드코딩.
2. Tooling 이 URI template 가정. OpenAPI, JSON Schema, generated SDK, request/response validator — 다 API 가 client 가 아는 안정 URI template 있다는 가정 중심으로 만들어짐. HATEOAS 채택은 이 tooling 대부분 포기 혹은 두 번 만들기.
3. 더 무거운 response. 모든 resource 가 크기를 두 배로 만들 수 있는 link 블록 운반. 높은 throughput API 엔 실제 bandwidth.
4. Testing 더 어려움. "/users/42 호출" 만 못 함 — root 에서 navigate, link 따라, user 찾아야. Testing tool 이 walk simulate 해야.
5. "URI 진화" 이득이 이론적. Production API 가 실전에서 URI 거의 안 바꿔; 대신 version (Track 2 lesson 5). HATEOAS 가 제공하는 탈출구가 대부분 팀이 안 가진 문제용.
대부분 API 가 실제 ship 하는 것 — 정직한 중간
현실 API 가 full HATEOAS commit 없이 일부 hypermedia 힌트 채택:
201 Created 의 Location header. 가장 보편적 hypermedia 힌트. 새 resource → URI 여기.
Pagination Link header (RFC 8288).Link: </items?cursor=abc>; rel="next". Client 가 URI 계산 대신 next-page link 따라. GitHub 가 이렇게 함.
Location 가진 redirect. 301/302/307/308 다 Location 에 다음 URI 운반. Hypermedia, lightweight.
Response 의 sub-resource URI. User resource 가 client 가 따라가게 강제 안 하고 편의로 {"orders_url": "..."} 포함할 수 있음.
이게 대부분 팀이 ship 하는 "REST API": resource-oriented URI, semantic 으로 쓰인 일곱 method, 진짜 뭔가 의미하는 status code, 본전 뽑는 곳에 hypermedia 힌트 한 줌. 순수 REST 아님. 순수 RPC 아님. 일관되고 실용적.
HATEOAS 는 도구지 미덕 아님. 비용이 본전 뽑는 곳에 써 (server-driven UI, 동적 workflow, 복잡 권한의 admin tool). 안 뽑는 곳에 건너뛰어 (전형적 CRUD API, 통합 중심 service). 둘 다 변호 가능. HATEOAS 없이 REST 라 부르고 그 선택을 그 용어로 변호하는 것도 변호 가능 — Fielding 이 순수 REST 아니라는 거 맞고; 네가 tradeoff 가 동작했다는 거 맞아.
HATEOAS 가 진짜 이기는 때
Server-driven UI. Spring HATEOAS, Hotwire/Turbo (HTML 기반), 혹은 hypermedia-driven 모바일 앱 같은 framework 가 client 재배포 없이 UI 변경 ship 하는 server-driven-link 접근 씀.
Workflow engine. 사용 가능 next-step 이 진짜로 resource state + user 권한 + 비즈니스 규칙에 의존할 때, server 의 link 로 표현하는 게 모든 client 에 state machine encode 하는 것보다 단순.
동적 권한 가진 admin tool. User 가 보는 버튼이 권한과 매칭해야. Server 가 auth 기반 link 포함/생략, client 는 render 만.
장수, 진화 API + 다양 client. Twilio 나 Stripe 면 recompile 못 하는 client 수천 — HATEOAS 가 URI 자유 주지만, generic client 도 ship 해야 하고, 그건 자체 프로젝트.
정직한 framing
API 가 "REST" 인지 논쟁 그만. 대신 design 선택 framing:
"이건 resource-oriented JSON-over-HTTP API. URI 가 명사 이름. Semantic 으로 일곱 HTTP method 씀. Status code 가 결과 전달. Client 가 URI template 하드코딩 + generated SDK 씀."
"이건 HATEOAS-스타일 link 가진 workflow API. Client 가 server 에서 사용 가능 전환 발견. HAL-formatted response 씀."
"이건 RPC-over-HTTP API. URI 가 operation 이름; 모든 거 POST; 구조화된 error 객체 돌려줌."
이 중 어느 거나 정직. 부연설명 없는 "REST API" 는 마케팅.
cwkPippa 의 정직한 label
cwkPippa API 는 "action endpoint 한 줌 가진 resource-oriented JSON-over-HTTP, HATEOAS 영." Frontend 가 호출하는 모든 URI 하드코딩. 201 의 Location header 가 유일 hypermedia 힌트. cwkPippa 에 맞는 shape — single client, lockstep 배포, 두 user 청중. 제 3자한테 공개 API ship 하면 계산이 더 많은 hypermedia 힌트 (pagination link, response 안 sub-resource URL) 쪽으로 뒤집힐 수도, 근데 아마 순수 HATEOAS 쪽으론 절대. 정직한 label 이 모두에게 논쟁 절약.
Code
Hypermedia 힌트 — 진짜 API 에 ship 되는 부분 REST·http
# 정직한 중간 — full HATEOAS 없는 hypermedia 힌트
# 1. 201 Created 의 Location header — 보편적 hypermedia 힌트
HTTP/1.1 201 Created
Location: /orders/o_xyz
Content-Type: application/json
{"id":"o_xyz","status":"pending"}
# 2. Pagination Link header (RFC 8288) — URI 계산 없이 next/prev 따라
HTTP/1.1 200 OK
Link: </items?cursor=abc>; rel="next", </items?cursor=zyx>; rel="prev"
Content-Type: application/json
{"items": [...], "cursor": "abc"}
# 3. 편의 sub-resource URL (강제 HATEOAS 아님, 그냥 helpful)
HTTP/1.1 200 OK
Content-Type: application/json
{
"id": "u_42",
"name": "Pippa",
"orders_url": "https://api.example.com/users/u_42/orders"
}
실용적 client — 하드코딩 URI + Location + Link header 인식·python
# 실용적 client — 하드코딩 URI + Location 과 Link header 존중
import httpx
class PragmaticClient:
"""현실 client: 하드코딩 URI template + opportunistic hypermedia."""
BASE = 'https://api.example.com'
def __init__(self, token: str):
self.client = httpx.Client(
base_url=self.BASE,
headers={'Authorization': f'Bearer {token}'},
)
def create_order(self, payload: dict) -> dict:
resp = self.client.post('/orders', json=payload)
resp.raise_for_status()
# Location header 존중 — 새 URI 계산 절약
new_url = resp.headers.get('Location')
return {'id': resp.json()['id'], 'url': new_url}
def list_orders_all_pages(self):
"""Page 자체 계산 대신 pagination Link header 따라."""
url = '/orders'
while url:
resp = self.client.get(url)
resp.raise_for_status()
for item in resp.json()['items']:
yield item
# RFC 8288 Link parsing — 있으면 next 따라
next_link = self._parse_link_header(resp.headers.get('Link', ''), 'next')
url = next_link
@staticmethod
def _parse_link_header(header: str, rel: str) -> str | None:
# 단순화된 parser; production: `linkheader` 같은 라이브러리
for part in header.split(','):
if f'rel="{rel}"' in part:
return part.split(';')[0].strip(' <>')
return None
HATEOAS 가 진짜 이기는 때 — server-driven workflow UI·python
# HATEOAS 가 이기는 때: server-driven workflow client
from dataclasses import dataclass
import httpx
@dataclass
class WorkflowAction:
name: str
method: str
href: str
class WorkflowClient:
"""이 client 는 link 읽는 법 빼고 workflow 에 대해 아무것도 모름."""
def __init__(self, entry: str):
self.client = httpx.Client()
self.entry = entry
def available_actions(self, resource_url: str) -> list[WorkflowAction]:
resp = self.client.get(resource_url).json()
actions = []
for rel, link in resp.get('_links', {}).items():
if rel in ('self', 'parent'):
continue
actions.append(WorkflowAction(
name=rel,
method=link.get('method', 'GET'),
href=link['href'],
))
return actions
def perform(self, action: WorkflowAction, payload: dict | None = None):
return self.client.request(action.method, action.href, json=payload).json()
# UI 가 available_actions() 항목 당 버튼 render — server 가 workflow control.
# Server 에 새 전환 가진 새 state 추가하면 코드 변경 없이 여기 버튼 추가.
# 이게 HATEOAS 가 제 값 하는 거.
통합한 API 셋 골라 (Stripe, GitHub, OpenAI, Slack, 네 거 — 이 중 셋 혹은 다른 거). 각각에 audit: (1) URI 가 SDK/client 가 하드코딩, 아니면 client 가 link 걸어? (2) Server 가 hypermedia 힌트 포함 (Location, Link, _links)? (3) Fielding 엄격 정의로 API 가 'REST' 라 공정 호칭 가능? 정직해. 그 다음 무겁게 유지/사용하는 API 하나에 한 문단 '정직한 label' 써 — 마케팅 자기 모습 아닌 실제 뭔지 묘사.
Hint
대부분 API 가 Fielding 테스트 fail. 괜찮 — 요점은 audit 이지 판단 아님. Stripe 와 OpenAI 둘 다 SDK 에 URI 하드코딩; 둘 다 201 에 Location 포함; 둘 다 자기를 REST 라 부름. GitHub 가 pagination Link header (진짜 hypermedia 힌트) 포함하는데 대부분 client 무시. cwkPippa 의 정직한 label: '단일 first-party React client 용 설계된 action endpoint 가진 resource-oriented JSON-over-HTTP, HATEOAS 영.' 네 것도 비슷.
Progress
Progress is local-only — sign in to sync across devices.