본문 바로가기
C.W.K.
Stream
Lesson 03 of 07 · published

HATEOAS — Fielding의 원형을 정확히 읽기

~12 min · rest-design, hateoas, fielding, pure

Level 0HTTP 입문자
0 XP0/46 lessons0/12 achievements
0/120 XP to next level120 XP to go0% complete
"REST API는 하이퍼텍스트가 주도해야 한다. 그렇지 않으면 RESTful하지 않다." — Roy Fielding, 2008. REST라는 이름이 자신의 정의와 다르게 쓰이는 상황에서, 2000년 박사논문의 뜻을 다시 분명히 밝힌 글이야.

박사논문을 정확히 읽기

Roy Fielding의 2000년 박사논문 Architectural Styles and the Design of Network-based Software Architectures는 REST를 클라이언트-서버, 무상태성, 캐시, 일관된 인터페이스, 계층형 시스템, 선택적인 code-on-demand 제약으로 설명해. HATEOAS는 별도의 여섯 번째 제약이 아니라 HATEOAS — Hypermedia as the Engine of Application State라는 이름으로 흔히 요약되는 일관된 인터페이스의 핵심 요소야. 오늘날 REST API라고 불리는 많은 설계가 바로 이 하이퍼미디어 제약을 충분히 적용하지 않아.

원칙은 이래. 클라이언트는 초기 진입 URI 하나와 표준화된 미디어 타입, 링크 관계와 동작의 의미를 알고 시작해. 그다음 가능한 동작과 관련 리소스, 다음 페이지 같은 상태 전환은 서버가 표현에 실어 보낸 하이퍼미디어를 통해 발견해. 클라이언트는 /users/{id}/orders를 임의로 조합하지 않고, /users/{id}를 GET 한 뒤 rel="orders" 링크가 가리키는 곳을 따라가. 'URI 하나만 알면 모든 의미를 저절로 이해한다'는 뜻은 아니야. 관계와 미디어 타입의 의미는 여전히 공유 계약으로 남아.

HTML 비유

HATEOAS를 이해하기 가장 좋은 사례는 HTML 웹이야. 브라우저는 어떤 사이트의 URL을 전부 미리 알지 못해. 첫 페이지를 받아 링크를 따라가고, 양식에 입력한 뒤 서버가 제시한 동작을 수행해. 애플리케이션 상태가 하이퍼미디어를 통해 전개되는 셈이야. 브라우저는 HTML의 링크와 양식 의미를 아는 범용 클라이언트고.

JSON 기반 API 같은 원리로 설계할 수 있어. 응답에 링크와 동작 설명을 넣고, 클라이언트는 합의된 관계 이름과 미디어 타입을 해석해 다음 요청을 선택해. 그러면 URI 구조에 대한 결합은 줄어들지만 사라지지는 않아. 관계 이름과 표현 형식의 계약은 안정적으로 유지해야 하고, 외부에 이미 공유된 URI의 호환성도 별도로 고려해야 해.

HATEOAS 응답은 어떻게 생길까

JSON 하이퍼미디어 형식은 여러 가지야. HAL(Hypertext Application Language)은 그중 널리 알려진 형식이고, 아래 예시는 HAL의 링크 구조에 동작 정보를 덧붙여 설명한 형태야. method 속성은 HAL 핵심 명세가 정한 표준 동작 표현이 아니라 이 예제의 확장이야:

{
  "id": "u_42",
  "name": "Pippa",
  "_links": {
    "self":   { "href": "/users/u_42" },
    "orders": { "href": "/users/u_42/orders" },
    "edit":   { "href": "/users/u_42", "method": "PUT" },
    "delete": { "href": "/users/u_42", "method": "DELETE" }
  }
}

클라이언트는 리소스를 읽고 _links에서 다음 탐색 경로를 찾아. 주문 목록으로 갈 때는 "orders" 관계를 따라가고, 수정이나 삭제처럼 입력과 메서드 정보가 필요한 동작은 해당 미디어 타입이 정의한 규칙으로 해석해야 해. 핵심은 URI 문자열을 추측해 조합하는 대신 서버가 제공한 전환을 따른다는 점이야.

HATEOAS는 클라이언트와 URI 구조의 결합을 줄여. 서버가 /users/{id}/orders/customers/{id}/purchases로 옮겨도 클라이언트가 orders 관계를 해석해 따라간다면 URI 템플릿을 바꿀 필요가 없어. 다만 관계 의미와 미디어 타입 계약은 그대로 유지되어야 하고, 저장된 북마크나 외부에 공유된 직접 URI까지 자동으로 보호되는 것은 아니야. 자유로운 변경이 아니라 결합 지점의 이동에 가까워.

왜 강력한가

  • URI 진화의 여지. 클라이언트가 관계 이름으로 이동하면 서버는 일부 내부 URI 구조를 바꿀 여지가 생겨. 안정된 관계 계약과 기존 직접 링크의 호환성은 여전히 관리해야 해.
  • 동적 기능 발견. 서버는 사용자 권한, 리소스 상태, 비즈니스 규칙에 따라 링크나 동작을 넣고 빼서 현재 가능한 전환을 응답에 직접 표현할 수 있어.
  • 범용 처리 가능성. 브라우저가 HTML의 표준 의미로 여러 사이트를 다루듯, 충분히 표준화된 JSON 하이퍼미디어 형식과 관계 어휘가 있다면 공통 클라이언트 계층을 만들 수 있어. 실제 범용성은 그 표준과 도구가 얼마나 널리 공유되는지에 달려 있고.
  • 작업 흐름 표현. 서버가 상태에 맞는 전환을 제공할 수 있어. 초안에는 "발행" 동작이 보이고, 발행 뒤에는 사라지는 식이야. 클라이언트는 허용된 동작을 표시하되, 의미 있는 UI와 입력 검증까지 모두 자동으로 얻는 것은 아니야.

작업 흐름 예

초안에서 검토를 거쳐 발행되는 글을 생각해 봐. 가능한 전환은 현재 상태에 따라 달라지고, HATEOAS 방식에서는 서버가 그 전환을 링크나 동작으로 표현해:

# Draft state
{
  "id": "a_42", "status": "draft", "title": "...",
  "_links": {
    "self":          { "href": "/articles/a_42" },
    "submit-review": { "href": "/articles/a_42/review",  "method": "POST" },
    "delete":        { "href": "/articles/a_42",         "method": "DELETE" }
  }
}

# Review 제출 후 — 다른 link
{
  "id": "a_42", "status": "in-review",
  "_links": {
    "self":   { "href": "/articles/a_42" },
    "approve":{ "href": "/articles/a_42/publish",  "method": "POST" },
    "reject": { "href": "/articles/a_42/reject",   "method": "POST" }
  }
}

클라이언트는 현재 제공된 전환을 보고 알맞은 버튼을 그릴 수 있어. 범용 동작 렌더러라면 새 관계를 어느 정도 자동 표시할 수 있지만, 새 동작의 문구와 입력 형식, 사용자 경험까지 이해하려면 미디어 타입의 표준 정의나 클라이언트 변경이 필요할 수 있어. HATEOAS는 상태 전환의 출처를 서버로 모으지, 도메인 의미 자체를 없애지는 않아.

형식이 중요한 이유

살펴볼 만한 JSON 하이퍼미디어 형식은 셋이야:

  • HAL(application/hal+json) — 링크와 포함된 리소스를 단순한 구조로 표현하는 잘 알려진 형식이야.
  • JSON:API(application/vnd.api+json) — 리소스와 관계, 페이지 나누기, sparse fieldsets 등에 강한 규칙을 둔 형식이야. 하이퍼미디어 사용 수준은 API 설계에 따라 달라져.
  • Siren(application/vnd.siren+json) — 입력 필드를 포함한 동작을 표현해 링크만 있는 형식보다 작업 흐름을 더 풍부하게 설명할 수 있어.

HTML은 지금까지 가장 성공적으로 배포된 하이퍼미디어 형식이야. 성공의 핵심은 링크만 존재한다는 데 있지 않아. 브라우저와 서버가 링크, 양식, 메서드, 미디어 타입의 의미를 폭넓게 공유한다는 데 있어.

cwkPippa의 HATEOAS 현실

cwkPippa API는 HATEOAS 방식으로 탐색하지 않아. 프런트엔드(frontend/src/lib/api.ts)가 호출할 URI를 알고 있고, 201 Created의 Location 헤더가 가장 가까운 하이퍼미디어 힌트야. 단일 React 프런트엔드가 같은 저장소에서 함께 배포되는 구조라면 이 선택이 충분히 합리적이야. HATEOAS를 추가하면 링크 모델과 클라이언트 해석 계층을 함께 유지해야 하므로, 얻는 결합 완화보다 비용이 클 수 있어. 다음 과에서 그 비용과 이득이 언제 뒤집히는지 살펴봐.

Code

HAL 스타일 응답 — 관계 이름과 포함된 리소스·json
// 순수 HAL response — link 가 모든 거 이끔
{
  "id": "u_42",
  "name": "Pippa",
  "email": "pippa@example.com",
  "_links": {
    "self":     { "href": "/users/u_42" },
    "orders":   { "href": "/users/u_42/orders", "title": "User 의 order" },
    "avatar":   { "href": "/users/u_42/avatar" },
    "edit":     { "href": "/users/u_42", "method": "PUT" },
    "delete":   { "href": "/users/u_42", "method": "DELETE" }
  },
  "_embedded": {
    "latest-order": {
      "id": "o_xyz",
      "_links": { "self": { "href": "/orders/o_xyz" } }
    }
  }
}
HATEOAS 클라이언트 — URI를 조합하지 않고 rel 따라가기·python
# HATEOAS client — 진입점 넘어 URI 절대 하드코딩 안 함
import httpx

class HateoasClient:
    def __init__(self, entry_point: str):
        self.entry = entry_point
        self.client = httpx.Client()

    def root(self):
        return self.client.get(self.entry).json()

    def follow(self, response: dict, rel: str, method: str = 'GET', **kwargs):
        """Relationship 이름으로 link 따라."""
        link = response['_links'].get(rel)
        if not link:
            raise RuntimeError(f'rel={rel} 의 link 없음; 사용 가능: {list(response["_links"].keys())}')
        method = link.get('method', method)
        return self.client.request(method, link['href'], **kwargs).json()

# 사용: client 가 /users/u_42/orders 명시적으로 절대 모름
client = HateoasClient('https://api.example.com/')
root = client.root()
user = client.follow(root, 'user-by-id', params={'id': 'u_42'})
orders = client.follow(user, 'orders')
for order_link in user['_links'].get('related-orders', []):
    detail = client.follow({'_links': {'self': order_link}}, 'self')
# Server 가 /users/u_42/orders 를 /customers/u_42/purchases 로 옮겼어?
# Server 가 'orders' rel update; 이 client 계속 동작.
하이퍼미디어 상태 기계 — 링크로 허용된 전환 표현하기·json
// 사용 가능 link 통한 state machine — 같은 resource, state 당 다른 link

// Draft article — submit-review 와 delete 만 가능
{
  "id": "a_42", "status": "draft",
  "_links": {
    "self":          { "href": "/articles/a_42" },
    "submit-review": { "href": "/articles/a_42/review", "method": "POST" },
    "delete":        { "href": "/articles/a_42",        "method": "DELETE" }
  }
}

// In-review article — 다른 전환
{
  "id": "a_42", "status": "in-review",
  "_links": {
    "self":    { "href": "/articles/a_42" },
    "approve": { "href": "/articles/a_42/publish", "method": "POST" },
    "reject":  { "href": "/articles/a_42/reject",  "method": "POST" }
  }
}

// Client 가 존재하는 link 에 매칭 버튼 render.
// 새 state 추가? 새 link 추가. Client 가 코드 변경 없이 적응.

External links

Exercise

리소스 서너 개가 있는 작은 JSON API를 하나 골라. 직접 만든 예제를 써도 좋아. 리소스 하나의 HAL 스타일 HATEOAS 응답을 스케치하고, 초기 진입 URL에서 시작해 URI를 조합하지 않은 채 rel 이름을 따라 특정 리소스까지 이동하는 클라이언트를 작성해 봐. 관계 이름이 바뀌거나 링크마다 인증 조건이 달라질 때 무엇이 필요한지도 적고, 고정 URI 템플릿을 쓰는 일반 클라이언트와 개발 경험을 비교해.
Hint
클라이언트에는 response._links[rel].href를 찾아 요청하는 follow(response, rel) 메서드가 필요해. 빠진 rel, 상대 URI 해석, 인증, 지원하지 않는 동작을 어떻게 다룰지도 생각해 봐. HATEOAS는 URI 조합을 줄이는 대신 관계 의미와 링크 처리 계층을 요구해. 다음 과에서는 이 비용이 실제 이득을 내는 조건을 따져 볼 거야.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고
💛 by 피파warm

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.