Contents

REST의 Uniform Interface 이해하기

우리는 RESTful 한 API 를 개발하려고 노력합니다. 근데 ‘왜’ RESTful 한 API 를 개발해야할까요? 바로 ‘독립적 진화’ 를 위해서 라고 할 수 있습니다. 서버와 클라이언트가 각각 독립적으로 진화하도록 하기 위해 RESTful 한 API 를 만들어 나가야 합니다.

이 글에서는 RESTful 한 API 를 개발하기 위해 꼭 알아야 하는 REST 의 제약조건 중 하나인 Uniform Interface 에 대해 정리해봤습니다.


REST는 분산 하이퍼 미디어 시스템(웹)을 위한 아키텍처 스타일입니다. 아키텍처 스타일이라는 것은 시스템의 구조와 구성 요소 간의 관계, 상호작용, 설계 원칙 등을 정의하는 일련의 원칙과 규칙의 집합인데요. REST 는 여러가지 제약 조건들로 이루어져 있어서 ‘하이브리드 스타일’이라고도 합니다.

Uniform InterfaceREST 의 제약조건 중 하나입니다. Uniform Interface 제약 조건 안에도 4가지 조건이 있습니다.

  1. Identification of Resources
  2. Manipulation of Resources Through Representations
  3. Self-Descriptive Messages
  4. Hypermedia as the Engine of Application State (HATEOAS)

하나씩 정리해보겠습니다. 혹시 Resource, Representation 에 대해 잘 모르고 있다면 리소스와 표현에 대해 먼저 이해하고 오시면 좋습니다.

1. Identification of Resources

첫번째 조건은 Resource 를 URI 로 식별할 수 있어야 한다는 것 입니다.

이 조건은 REST 의 Resource 에 대한 조건과도 같습니다. REST 에서는 Resource 가 하이퍼텍스트의 참조 대상이 될 수 있어야 한다고 하는데 같은 조건이라는 생각이 됩니다.

REST 에서는 URI 가 최대한 변경되지 않는 것을 기대한다고 합니다. 모든 것은 바뀐다는 말 빼고 다 바뀐다 라는 말을 어디선가 본 적이 있는데 어려운 말인 것 같습니다 ^^; 최대한 노오력을 해서 URI 변경을 최소화 해야할 것 같습니다.

2. Manipulation of Resources Through Representations

두번째 조건은 표현을 통한 리소스의 조작이라고 하는 것 입니다.

표현(Representation)이라는 것은 리소스의 현재 상태 혹은 의도된 상태를 담고 있는 정보인데요. 클라이언트와 서버가 이 ‘표현’ 이라는 것을 주고 받으면서 리소스를 생성 / 수정 / 삭제 하는 것을 표현을 통한 리소스의 조작이라고 합니다.

우리가 주로 사용하는 Http Message 인 Request, Response 가 표현이 될 수 있습니다. 아래는 간단한 예시입니다.

POST /posts HTTP/1.1 
Host: example.com 
Content-Type: application/json 

{ 
  "title": "제목", 
  "content": "내용", 
  "uid": 154234 
}

위의 표현으로 블로그 포스트 리소스를 생성하고 있습니다. POST 메서드 자체가 표현이 되는 것이 아니고 Request 의 헤더, 본문까지 모두 해서 ‘표현’이 됩니다.

참고로 표현은 표현 데이터 + 표현 데이터의 메타 데이터로 구성이 되는데, 헤더가 표현 데이터의 메타 데이터, 본문이 표현 데이터가 되겠습니다.

아래는 수정, 삭제를 하는 ‘표현’ 예시입니다.

PUT /posts/123 HTTP/1.1 
Host: example.com 
Content-Type: text/plain 

수정된 블로그 포스트 내용.
DELETE /posts/123 HTTP/1.1 
Host: example.com

3. Self-Descriptive Messages

REST 논문에서는 Self-Descriptive Messages 에 대해 아래와 같이 말하고 있습니다.

REST constrains messages between components to be self-descriptive in order to support intermediate processing of interactions.

컴포넌트들 간의 메세지가 중간 처리를 위해 스스로 설명(self-descriptive)이 가능해야 한다고 말하고 있습니다. 메시지가 어떤 종류의 데이터를 가지고 있는지, 어떤 형식으로 표현되었는지, 어떤 언어로 작성되었는지 등의 정보를 메시지 자체에서 제공하는 것 입니다. 메시지를 받은 컴포넌트는 추가적인 정보 없이도 그 메시지가 무엇을 의미하는지를 이해할 수 있습니다.

아래 예시를 보면, 클라이언트가 서버로부터 받는 응답 메세지에는 Content-Type, Content-Language 헤더를 사용해서 스스로 어떤 유형의 데이터인지, 어떤 언어로 표현되어 있는지 설명합니다.

HTTP/1.1 200 OK 
Content-Type: application/json 
Content-Language: en-US 
Date: Mon, 08 Aug 2023 15:30:00 GMT 
Server: SampleServer 

{ 
  "message": "Hello, world!" 
}

Message 와 Representation 는 같은 것일까요? 로이 필딩은 Http Message 가 Representation 이라고 하기엔 덜 정확하다고 했습니다. Representation 에는 Document 나 File 등도 포함되어 있는 것을 생각해보면 Message 와 Representation 은 분리해서 생각해야할 것 같습니다.

메세지의 목적에 따라 조금씩 다르겠지만 아래와 같은 헤더 요소가 포함되는 것이 좋다고 합니다. (feat. ChatGPT)

  1. Content-Type: 메시지 본문의 데이터 형식을 지정합니다. 예를 들어, JSON, XML, 텍스트 등이 있습니다.
  2. Content-Language: 메시지의 내용이 작성된 언어를 지정합니다. 이를 통해 수신자는 어떤 언어로 된 데이터를 받았는지를 알 수 있습니다.
  3. Content-Length: 메시지의 본문 길이를 나타냅니다. 이를 통해 수신자는 메시지의 크기를 파악하고 적절하게 처리할 수 있습니다.
  4. Cache-Control: 메시지의 캐싱 정책을 지정합니다. 캐싱은 중간 컴포넌트들이 메시지를 저장하고 재사용하는데 관련된 설정입니다.
  5. Vary: 메시지가 서로 다른 상황에서 다른 표현을 가지는 경우, 어떤 요소에 따라 표현이 달라질 수 있는지를 지정합니다. Content Negotiation 과 관련됩니다.
  6. Etag: 엔터티(리소스)의 버전을 나타내는 태그 값입니다. 캐싱 및 조건부 요청과 관련이 있습니다.
  7. Last-Modified: 엔터티가 마지막으로 수정된 시간을 나타냅니다. 조건부 요청과 캐싱과 관련이 있습니다.

한가지 더 살펴볼 것이 있습니다. 위의 “Hello, World” 예제는 Self-Descriptive 할까요? 그렇다고 하기엔 아쉬운 부분이 있습니다. 바로 응답 본문인 JSON 데이터 때문입니다.

Content-Type 을 보고 본문을 JSON 형식으로 파싱을 하면 되겠구나! 하는 것은 알 수 있지만 키 값인 “message” 가 무엇을 뜻하는지는 알 수가 없습니다. “message” 는 쉽게 유추가 가능하겠지만 “something_special” 같은 설명이 필요한 키가 있다면 이해하기 어렵습니다. 이런 문제를 해결하는 두 가지 방법을 찾아봤습니다.

  • MediaType 정의 - IANA 에 미디어 타입을 정의해 등록한다.
  • Profile 이용하기 - Link 헤더에 rel=“profile” 을 하고, 본문의 데이터에 대한 명세를 링크하도록 한다.
    • 다만 이 방법은 웹 클라이언트가 RFC 6906 을 이해해야 하는데 아직(2023.08) 지원을 잘 못하고 있는 것 같다.
    • https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel, https://www.w3.org/TR/html40/types.html#h-6.7 에서도 profile 에 대한 정보를 찾을 수 없는데 아직 지원이 미비한 것 같다. (제가 잘 못찾은 것일 수 있습니다. 찾으시면 알려주세요..!)
    • 대체 방법으로 HAL(Hypertext Application Language) 이라는 스펙을 사용해서 링크를 제공할 수 있다. HAL 은 쉽게 생각해서 외부 링크를 담고 있는 JSON 이나 XML 문서를 생각하면 될 것 같다.

다만 로이 필딩은 MediaType 등록이 필수이냐는 질문에 API 사용자들이 본문에 대해 이미 잘 알고 있다면 굳이 할 필요가 없다 라고 합니다. REST API 의 목적과 환경에 따라 다르게 적용하면 될 것 같습니다.

Self-Descriptive 를 만족하게 되면 서버나 클라이언트가 변경 되더라도 주고 받는 메세지에 대해 해석이 가능해집니다. 때문에 Uniform Interface 의 목적인 독립적인 진화에 도움을 주게 됩니다.

4. Hypermedia as the Engine of Application State (HATEOAS)

HATEOAS 는 애플리케이션의 상태가 하이퍼미디어를 통해 다른 상태로 바뀐다는 것 입니다. 하이퍼미디어는 이미지, 동영상, 텍스트 등 다른 형태의 미디어로 연결되는 링크가 포함된 모든 콘텐츠를 의미합니다.

사용자는 하이퍼미디어(주로 하이퍼링크)를 통해 원하는 상태로 이동이 가능합니다.

보통 웹 쇼핑을 할 때 원하는 상품의 상세 정보를 보기 위해 상품의 사진이나 상품명을 클릭해서 상세 정보를 보러 들어가곤 합니다. 또 상품을 주문하기 위해 ‘주문하기’ 버튼을 클릭해 결제를 하고, 주문 완료가 되면 ‘주문 내역을 확인하기’ 같은 버튼을 통해 주문 내역을 확인하러 갑니다.

우리는 이미 정보를 보거나, 생성하거나 하는 행위를 하이퍼미디어를 통해서 하고 있습니다. 다만 내부에서 이게 어떻게 동작하고 있는지를 봐야겠죠!

결국 HATEOAS 를 달성하는 방법을 알아야 할 것 같습니다. 가장 중요한 것은 링크를 포함한 응답을 만들어내는 것이라고 생각이 됩니다.

보통 HTML 을 생성해서 응답할 때는 <a> 태그에 다음 상태로 갈 수 있는 링크가 들어가 HATEOAS 를 만족합니다. JSON 이나 XML 을 생성해서 응답할 때가 HATEOAS 를 만족하지 못하는 경우가 많은 것 같습니다. 어떻게 HATEOAS 를 만족시킬 수 있을까요? 저는 두 가지 방법을 찾아봤습니다.

  • RFC 5988 (web linking)
  • Hypertext API Language (HAL)

RFC 5988 (web linking)

RFC5988은 웹에서 리소스 간의 관계를 정의하는 프레임워크를 제안한다고 합니다. Link 헤더에 다음 상태로 갈 수 있는 여러 링크 정보를 넣어 HATEOAS 를 달성합니다. 링크가 <> 안에 들어가는 것이 특이한 것 같습니다. rel 은 relation 의 줄임말인데 rel 로 리소스 간의 관계를 나타낼 수 있습니다. 콤마 , 로 이어서 여러 링크를 추가할 수 있습니다.

Link: <http://example.com/TheBook/chapter2>; rel="previous";
         title="previous chapter"
Link: </>; rel="http://example.net/foo"
Link: </TheBook/chapter2>;
         rel="previous"; title*=UTF-8'de'letztes%20Kapitel,
         </TheBook/chapter4>;
         rel="next"; title*=UTF-8'de'n%c3%a4chstes%20Kapitel

Hypertext API Language (HAL)

HAL 은 외부 리소스에 대한 링크와 같은 하이퍼미디어를 JSON 이나 XML 에 표현하기 위한 규칙입니다. HAL 을 표현하는 미디어타입은 아래와 같습니다.

Content-Type: application/hal+xml 
Content-Type: application/hal+json

HAL 의 spec 을 정의하고 있는 https://stateless.group/hal_specification.html 에 나온 예시입니다.

{
    "_links": {
        "self": { "href": "/orders" },
        "curies": [{ "name": "ea", "href": "http://example.com/docs/rels/{rel}", "templated": true }],
        "next": { "href": "/orders?page=2" },
        "ea:find": {
            "href": "/orders{?id}",
            "templated": true
        }
    },
    "currentlyProcessing": 14,
    "shippedToday": 20,
    "_embedded": {
        "ea:order": [{
            "_links": {
                "self": { "href": "/orders/123" },
                "ea:basket": { "href": "/baskets/98712" },
                "ea:customer": { "href": "/customers/7809" }
            },
            "total": 30.00,
            "currency": "USD",
            "status": "shipped"
        }]
    }
}

HAL 은 Resource 와 Link 두 가지 간단한 개념을 표현하는데 중점을 둔다고 합니다.

  • Resource
    • Links: 여러 링크 정보를 말합니다. (_links 필드)
    • Embedded Resources: 해당 리소스에 포함된 기타 리소스에 대한 정보입니다.
    • State: JSON, XML 데이터의 상태 데이터를 말합니다.
  • Links
    • A Target (a URI)
    • A relation: 리소스와의 관계를 나타냅니다. 해당 리소스 자체를 나타낼 때는 self 를 사용합니다. 참고로 모든 resources 는 스스로에 대한 링크 정보를 갖고 있어야 한다고 합니다.
    • A few other optional properties to help with deprecation, content negotiation, etc.

https://stateless.group/assets/info-model.png

_link 를 작성할 때 참고할 간단한 규칙 같은 것들이 있는데 https://stateless.group/hal_specification.html 여기를 참고하면 될 것 같습니다.

HATEOAS 를 위해 링크를 포함한 응답을 작성하게 되면 서버 내에서 링크가 변경되어도 클라이언트에서는 변경할 것이 없습니다. HATEOAS 를 지키려고 하다보면 자연스럽게 서버와 클라이언트 각각 독립적인 발전이 가능해질 것 같습니다.


REST 는 추상적인 부분이 많아서 정리하는 것이 굉장히 어려웠던 것 같습니다. 조금이라도 Uniform Interface 에 대해 이해하는데 도움이 되었으면 좋겠습니다.

References