본문 바로가기
Development

[Network] 그런 REST API로 괜찮은가? 진짜?

by 한휴르 2023. 1. 10.

REST API에 대해서 공부를 하려고 찾다보면 자주 보이는 문구가 있습니다.

그런 REAT API로 괜찮은가?

2017년 Deview에서 이응준님이 발표하신 세션의 제목입니다.

https://www.youtube.com/watch?v=RP_f5dMoHFc

해당 영상을 보면 REST API에 대한 설명과 어떻게 설계하는게 좋은지에 대한 내용이 잘 담겨져 있습니다.
위 동영상 내용을 정리하면서 REST API란 무엇이고, RESTful하다고 불리기 위해서는 어떻게 설계를 해야하는지 알아보겠습니다.

1. 서두

REST라는 이름은 웹 개발자라면 듣지 않을 수가 없는 단어입니다. API를 만들다보면 REST하지 않다는 이야기도 하고, 하다못해 면접을 보러가도 REST에 대해서 설명해보라는 질문을 받습니다. 하지만 REST가 뭔지 확실히 말할 수 있는 경우는 잘 없습니다. 어렴풋이 연기처럼 존재하지만 잡히지 않는 상태로 머리 속에 남아 있기에 잘 설명을 못하는 경우가 많습니다. 그러면 도대체 이 REST란게 무엇일까요?

1) REST란?

REST는 REpresentational State Transfer의 약자입니다. 이것만 보고 어떤 것인지 느낌이 오시나요? 온다면 당신은 천재입니다. 저는 전혀 오지 않으니 좀 더 자세한 정의를 가져와 보겠습니다.

a way of providing interoperability between computer systems on the Internet.

interoperablility는 "상호 운용성"이라는 뜻으로, 컴퓨터 시스템와 인터넷 사이에 상호 운용성을 제공하는 방법이라고 합니다. 여기까지 봐도 잘 모르겠습니다.
그렇다면 REST가 어떠한 배경에서 나오게 되었는지 그 계기를 통해서 좀 더 이해해보도록 하겠습니다.

2) REST의 출현 계기

첫 시작은 Web입니다. 1991년에 www가 팀 버너스 리에 의해 탄생했습니다. 그런데 여기서 하나의 고민이 존재했습니다.

어떻게 인터넷에서 정보를 공유할 것인가?

이에 대한 해답으로 "Web"이 출범하게 됩니다. 이에 팀 버너스 리의 답은 아래와 같습니다.

정보들을 하이퍼 텍스트로 연결한다.
표현 형식 : HTML
식별자 : URI
전송방법 : HTTP

이제 HTTP라는 프로토콜을 여러 사람들이 설계를 하게 되었습니다. 그 중에 1명, 대학원생이었던 로이 필딩(Roy T. Fielding)이라는 사람이 이 프로토콜 작업에 참여하게 됩니다.
로이 필딩은 참여하는 와중에 고민이 생깁니다. 이미 94년도에 로이는 http 1.0 작업에 참여했습니다. 이 명세가 나오기 전에 이미 http는 당연히 www의 전송 프로토콜로서 이용이 되고 있었습니다. 그리고 또한 웹은 이미 급속도로 성장하는 도중이었습니다.

이 시점에서 로이는 http를 정립하고 이 명세에 기능을 더하고 기존의 기능을 고쳐야하는 상황에 놓이게 됩니다. 그러나 무작정 http 프로토콜을 고치게 된다면, 기존 구축되어 있는 웹하고 호환이 안되는 가능성이 존재했습니다. 이에 로이는 고민에 빠집니다.

"How do I improve HTTp without breaking the Web?"

"웹을 망가뜨리지 않고 어떻게 http 기능을 증가시킬 수 있을까?". 로이는 고민 끝에 HTTP Object Model이라는 것을 만들게 됩니다. 아직은 REST가 아닙니다. 이를 4년 후 "Representational State Transfer: An Architectural Style for Distributed Hypermedia interaction"에서 REST로 최초 공개합니다. 이후 2년 후, "Architectural Styles and the Design of Network-based Software Architectures"을 박사 논문으로 발표하게 됩니다. 약 120페이지 가량의 이 박사 논문이 바로 그 REST라는 것을 정의하는 논문입니다.

3) API

그런 한편, API(Application Programming Interface)라는 것이 있습니다. 98년에 마이크로소프트에서 원격으로 다른 시스템에 메소드를 호출할 수 있는 XML-RPC(1998) 프로토콜을 만들었습니다. 이는 곧 SOAP이라는 이름으로 바뀌고, Salesforce라는 회사가 인터넷에 최초로 공개하게 됩니다. 당시 SOAP을 사용해서 API를 만들었는데 너무 복잡했습니다.

아래의 코드는 아이디로 어떤 Object 하나를 가져오는 요청 메시지입니다.

<?xml version="1.0" encoding="utf-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
    xmlns:urn="urn:enterprise.soap.sforce.com">
    <soapenv:Header>
        <urn:SessionHeader>
            <urn:sessionId><b>QwWsHJyTPW.1pd0_jXlNKOSU</b></urn:sessionId>
        </urn:SessionHeader>
    </soapenv:Header>
    <soapenv:Body>
            <urn:fieldList><b>Id, Name, Website</b></urn:fieldList>
            <urn:sObjectType><b>Account</b></urn:sObjectType>
            <!--Zero or more repetitions:-->
              <urn:ids><b>001D000000HS2Su</b></urn:ids>
            <urn:ids><b>001D000000HRzKD</b></urn:ids>
        </urn:retrieve>
    </soapenv:Body>
</soapenv:Envelope>

너무 복잡하여 Salesforce의 API는 많이 쓰이지 않게 됩니다. 그리고 4년 후에 flickr라는 회사에서 SOAP을 사용하여 API를 발표하는데 훨씬 간단해 졌습니다.

SOAP

<?xml version="1.0" encoding="utf-8" ?>
<s:Envelope xmlns:s="http://www.w3.org/2001/06/soap-envelope">
    <s:Body>
        <s:Fault>
            <faultcode>flickr.error.[error-code]</faultCode>
            <faultstring>[error-message]</faultstring>
            <faultactor>http://www.flickr.com/services/soap/</faultactor>
            <details>Please see http://www.flickr.com/services/docs/ for more details</details>
        </s:Fault>
    </s:Body>
</s:Envelope>

그리고 같은 역할을 하는 API인데 논문에 나온 내용을 바탕으로 작성하여 REST라는 이름으로 발표했습니다.

REST

<?xml version="1.0" encoding="utf-8" ?>
<rsp stat="fail>
        <err code="[error-code]" msg="[error-message]" />
</rsp>

한 눈에 보더라도 훨씬 짧아졌습니다. 이에 사람들이 아래와 같이 느끼게 됩니다.

SOAP → 복잡, 규칙 많음, 어렵다.
REST → 단순, 규칙 적음, 쉽다.

그 결과 SOAP의 사용량은 계속 감소하고 REST의 사용량은 계속 증가하게 되었습니다.

결국, 2006년 AWS가 자사 API 사용량의 85%가 REST임을 밝히고 2010년에 Salesforce.com조차 REST API를 추가했습니다. 즉, REST가 완전히 승리하게 되었습니다. 이렇게 WWW의 API가 행복하게 REST로 정착이 되나 싶었습니다.

하지만...

4) 그것은 REST가 아니다.

2008년 CMIS라는 것이 나왔습니다. 이것은 CMS를 위한 표준으로 EMC, IBM, 마이크로소프트와 같은 기업들이 함께 작업했으며, REST 바인딩을 지원한다고 발표했습니다.
하지만, 이것을 본 로이 필딩이 이런 말을 합니다.

"No REST in CMIS"
CMIS에는 REST가 없다.

다른 사람들이 볼 때는 충분히 REST처럼 보였지만, 정작 논문 저자인 로이는 아니라고 말했습니다. 그리고 또 이런 일도 있습니다.

2016년 말, 마이크로소프트가 REST API 가이드 라인을 만들었습니다.

URI는 https://{serviceRoot}/{collection}/{id} 형식이어야 함.
GET, PUT, DELETE, POST, HEAD, PATCH, OPTIONS를 지원해야함.
API 버저닝은 Major.minor로 하고 URI에 버전 정보를 포함시킨다.
...

좋은 API에 잘 부합하는 것 같았습니다. 하지만 로이는 트위터에 이렇게 말합니다.

"s/REST API/HTTP API/"
이것도 REST API가 아니다. 이것은 그냥 HTTP API다.

그리고 또한 블로그에 아래와 같은 글도 씁니다.

"REST API must be hypertext-driven"
REST API는 반드시 hypertext-driven이어야 한다.

또 어디가서 이런 발표를 합니다.

"REST API를 위한 최고의 버저닝 전략은 API 버저닝을 안하는 것"

뭔가 사람들이 알고 있던 REST API와 정작 REST를 만들었던 로이의 REST API은 너무나도 달랐습니다.
도대체 무엇이 문제일까요? 왜 이런 차이가 있는걸까요?

2. 본문

1) REST API

REST API라고 하면 말 그대로 REST 아키텍처를 따르는 API입니다. 그렇다면 REST는 뭐냐? 로이 필딩의 논문에는 이렇게 나와있습니다.

REST란 분산 하이퍼미디어 시스템(ex. 웹)을 위한 아키텍처 스타일

그렇다면 아키텍처 스타일은 무엇일까요? REST는 아키텍처 스타일이고 아키텍처 스타일은 제약 조건의 집합입니다.
즉, 제약 조건을 모두 지켜야 REST를 따른다고 말할 수 있다는 것입니다.

REST는 아키텍처 스타일이면서 동시에 하이브리드 아키텍처 스타일이라고 말합니다. 왜냐하면 아키텍처 스타일이면서, 동시에 아키텍처 스타일의 집합이기 때문입니다.

2) REST를 구성하는 스타일

REST는 6가지 아키텍처 스타일로 이루어져 있고 그것은 다음과 같습니다.

  • Client-Server
  • Stateless
  • Cache
  • Uniform Interface
  • Layered System
  • Code-on-Demand (optional)

오늘 날 대체로 REST라고 부르는 것들은 위의 조건을 대부분 지키고 있습니다. 왜냐하면 HTTP만 잘 따라도 Client-Server, Stateless, Cache, Layered System은 다 지킬 수 있기 때문입니다. Optional이라고 되어 있는 'Code-on-Demand'는 서버에서 코드를 클라이언트로 보내서 실행할 수 있어야 한다는 것으로, 즉 자바스크립트를 이야기 하는 것입니다.

하지만 로이가 REST가 아니라고 말하는 것들은 대부분 Uniform Interface를 만족하지 못하는 것입니다. Uniform Interface 역시 아키텍처 스타일인데 안에 뭐가 들어있는지 살펴보겠습니다.

3) Uniform Interface 제약 조건

Uniform Interface는 네 가지 제약 조건으로 이루어져 있습니다.

  • Identification of resources
  • Manipulation of resources through representations
  • Self-descriptive messages
  • Hypermedia as the engine of application state(HATEOAS)

Identification of resources은 리소스가 URI로 식별되야 한다는 것이고, Manipulation of resources through representations는 representation 전송을 통해서 리소스를 조작해야 된다는 것입니다. 즉, 리소스를 만들거나 수정, 삭제할 때 http 메시지에 그 표현을 담아서 전송해야된다는 것입니다. 보통 2가지 조건은 잘 지켜지고 있습니다. 나머지 2개가 문제인데, 이 2가지는 REST API라고 부르는 거의 모든 것들이 지키지 못하고 있습니다.

4) Self-descriptive messages

Self-descriptive messages는 말 그대로 "메시지가 스스로를 설명할 수 있어야 한다"는 것입니다.

예를 들어 아래와 같은 메시지가 있다고 해보겠습니다.

GET / HTTP/1.1

단순히 루트를 얻어오는 GET 요청입니다. 이 HTTP 요청 메시지는 뭔가 빠져 있어서 Self-descriptive 하지 못합니다. 바로 목적지가 빠져있습니다.
목적지를 추가해주도록 하겠습니다.

GET / HTTp/1.1
Host: www.example.org

이렇게 요청이 www.example.org라는 도메인으로 간다는 목적지가 추가되면 이 메시지는 Self-descriptive하다고 할 수 있습니다.

또 이런 것도 생각해볼 수 있습니다. 200 응답 메시지이며, JSON 본문이 있습니다.

HTTP/1.1 200 OK
[ { "op": "remove", "path": "/a/b/c" } ]

이렇게 되면 당연히 Self-descriptive 하지 않다고 합니다. 왜냐하면 이걸 클라이언트가 해석하려고 하면, 어떤 문법으로 작성된 것인지 모르기 때문에 해석에 실패합니다. 그렇기 때문에 Content-Type 헤더가 반드시 들어가야 합니다.

HTTP/1.1 200 OK
Content-Type: application/json

[ { "op": "remove", "path": "/a/b/c" } ]

Content-Type 헤더에서 application/json이라고 되어 있어서 대괄호, 중괄호, 큰따옴표의 의미를 이해하게 되고 파싱이 가능해지면서 문법을 해석할 수 있게 됩니다.
그렇다면 이제 Self-descriptive하다고 볼 수 있을까요? 아닙니다. 그걸 해석했다고 하더라도, op 값은 무슨 뜻이고, path가 무엇을 의미하는지는 알 수 없습니다.

HTTP/1.1 200 OK
Content-Type: application/json-patch+json

[ { "op": "remove", "path": "/a/b/c" } ]

이렇게 명시해주면 완전해집니다. 이 응답은 json-patch+json이라는 미디어 타입으로 정의된 메시지이기 때문에 json-patch라는 명세를 찾아가서 이해한 다음, 이 메시지를 해석하면 그제서야 올바르게 메시지의 의미를 이해할 수 있게 됩니다.

이처럼 Self-descriptive message라는 것은 메시지를 봤을 때 메시지의 내용으로 온전히 해석이 다 가능해야된다는 것입니다.
하지만 오늘 날의 REST API는 대부분 만족하지 못하고 있습니다. 미디어 타입에는 그냥 json이라고만 되어있고 이를 어떻게 해석해야되는지는 명시가 되어있지 않습니다.

5) HATEOAS

Hypermedia as the engine of application state, 줄여서 HATEOAS라고 읽는 이 제약 조건은 애플리케이션의 상태는 Hyperlink를 이용해 전이되어야 한다는 것입니다.

예를 들어 아래와 같은 사이트가 있다고 해보겠습니다.

단순한 게시판인데 다음과 같은 플로우로 동작합니다.

루트 홈페이지 → 글 목록 보기 GET → 글 쓰기 GET → 글 저장 POST → 생성된 글 보기 GET → 목록 보기 GET → ...

이렇게 상태를 전이하는 것을 애플리케이션 상태 전이라고 하고, 이 상태 전이마다 항상 해당 페이지에 있던 링크를 따라가면서 전이했기 때문에 HATEOAS라고 할 수 있습니다. 말 그대로, 하이퍼 링크를 통한 전이가 된 것입니다.

그래서 html 같은 경우를 보면 HATEOAS를 만족하게 되는데,

HTTP/1.1 200 OK
Content-Type: text/html

<html>
<head> </head>
<body> <a href="/test"> test </a> </body>
</html>

a 태그를 통해서 하이퍼링크가 나와 있고, 이 하이퍼 링크를 통해서 그 다음 상태로 전이가 가능하기 때문에 만족한다고 볼 수 있습니다.

Json으로 표현할 때 어떻게 만족 할 수 있을까요?

HTTP/1.1 200 OK
Content-Type: application/json
Link: </articles/1>; rel="previous",
      </articles/3>; rel="next";

{
    "title": "The second article",
    "contents": "blah blah..."
}

Link라는 헤더가 있는데, 이것이 바로 이 리소스와 하이퍼링크로 연결되어 있는 다른 리소스를 가르킬 수 있는 기능을 제공해줍니다.

여기서 어떤 1개의 게시물을 표현 했는데, 이전의 게시물 URI가 /articles/1, 다음 게시물은 /articles/3에 있다는 정보를 표현해준 것입니다. 또한, 이 정보는 Link 헤더가 이미 표준으로 문서가 나와 있기 때문에 이 메시지를 보낸 사람이 온전히 해석해서, 어떻게 링크가 되어 있는가를 이해하고, 하이퍼링크를 타고 다른 상태로 전이가 가능합니다. 따라서 HATEOAS를 만족한다고 할 수 있습니다.

6) 왜 Uniform Interface?

그럼 왜 이러한 Uniform Interface가 필요할까요?

바로 독립적 진화를 하기 위해서입니다. 독립적 진화란 것은 서버와 클라이언트가 각각 독립적으로 진화한다는 것입니다.
독립적으로 진화한다면 어떻게 될까요? 서버의 기능이 바뀌었을 때 새로운 API가 추가되고 기존 API가 변경/삭제되고 URI가 바뀌고 변동이 있습니다. 하지만 클라이언트는 바뀌지 않아도 됩니다.
즉, 독립적으로 진화를 하게 되면 서버의 기능이 변경되어도 클라이언트를 업데이트할 필요가 없습니다. 이것을 독립적인 진화라고 합니다.

이게 바로 REST를 만들게 된 계기입니다. HTTP 1.0을 만들 때 로이 필딩이 고민했던 것 "How do I imporve HTTP without breaking the Web", "웹을 망가뜨리지 않고 어떻게 수정할 것인가"에 대한 결과가 REST입니다.

그렇기 때문에 REST가 목적하는 바가 독립적인 진화입니다. 이를 달성하기 위해서는 Unifrom Interface가 반드시 만족해야 하기 때문에, 이를 만족하지 못하면 REST라고 부를 수 없게 되는 것입니다.

실제로 잘 지켜지고 있는가?

그렇다면 실제로 잘 지켜지고 있을까요? REST를 아주 잘 지키고 있는 사례는 바로 웹입니다.

웹 페이지를 변경했다고 웹 브라우저를 업데이트할 필요는 없다.
웹 브라우저를 업데이트 했다고 웹 페이지를 변경할 필요도 없다.
HTTP 명세가 변경되어도 웹은 잘 작동한다.
HTML 명세가 변경되어도 웹은 잘 작동한다.

우리가 들어가는 웹 페이지가 변경되었다고 해서 그 웹 페이지를 접근할 때 웹 브라우저를 업데이트 할 필요 없습니다. 그냥 잘 들어가집니다.
반대로 크롬을 업데이트 했다고 해서, 웹 페이지에 접속이 안되는 것도 아닙니다.

심지어 HTTP 명세가 바뀌어도 그렇습니다. 2014년 HTTP 2.0이 나오고 여러가지 웹 페이지들이 HTTP 2.0을 적용합니다. 하지만 적용을 하지 않은 사이트들도 여전히 잘 됩니다.

마찬가지로 HTML 5.0이 HTML 5.2로 바뀌어도 똑같이 잘 됩니다. 물론 옛날 버전에서는 페이지가 좀 깨질 수는 있습니다.
하지만 페이지가 깨진다고 기능은 동작 안하는 것은 아닙니다. 웹에서 제공하는 기능들은 다 정상적으로 동작을 합니다.

그런데 우리가 만든 모바일 앱은 많은 문제들을 겪고 있습니다. 이것은 무슨 이야기일까요?

앱이 강제 업데이트 하는 경우가 있습니다. 서버가 기능이 변경되어 있는데, 클라이언트가 그것을 지원해주는데 한계가 있을 때 업데이트를 진행하는 것입니다. 그런데 이게 되게 잦다는 것입니다.
웹에서는 이런 경우가 잘 없습니다. 하지만 모바일 앱에서는 자주 있는 일입니다. 엄밀히 말하면 우리가 만든 모바일 앱 클라이언트와 서버가 REST 아키텍처 스타일을 따르고 있지 않다고 말할 수 있습니다.

웹은 어떻게 이런 것이 가능하게 했을까요? 한 방에 해결해주는 은탄환 같은 것은 없습니다. 당연히 많은 노력을 했습니다.
W3C Working group에서 HTML을 만들고, IEFT Working groups에서 HTTP를 만들고, 웹 서버/브라우저 개발자들이 많은 토론을 하며 노력을 합니다.

HTML5 첫 초안에서 권고안이 나오는데 무려 6년, HTTP/1.1 명세 개정판 작업하는데 7년이 걸렸습니다. HTTP/1.1에 새로운 기능이 추가 되었을까요? 아닙니다. 새로운 기능은 하나도 없었고 문서를 다듬기만 하는데 7년이 걸렸습니다. 절대로 하위 호환을 깨드리지 않기 위한 정말 깊은 토론의 결과입니다.

7) 상호운용성(interoperability)에 대한 집착

Referer → 오타지만 안 고침
charset → 잘못 지은 이름이지만 안 고침
HTTP 상태 코드 418(I'm a teapot) 포기
HTTP/0.9 아직도 지원(크롬, 파이어폭스)

25년 전 Referer 오타를 냈지만 이를 고치지 않았습니다. 이를 고치는 순간 웹이 깨지기 때문입니다. 또 charset이란 이름은 잘못 지었습니다. 원래 encoding이라고 이름을 지어야했는 데 당시 encoding 개념이 부족하여, character set이랑 같은 의미로 생각하고 지었습니다. 하지만 이름을 고치면 상호 운용성이 깨지기 때문에 그대로 갔습니다.

최근에 HTTP 상태 코드가 하나씩 추가되다가 418번을 추가할 때가 되었는데, 문제가 생겼습니다.
과거 만우절 때 만들었던 이상한 spec이 하나 있는데, HTCPCP 상태 코드, 엄밀히 말하면 이것은 HTTP가 아니라서 무시를 해도 됐습니다. 하지만 수많은 node.js나 Golang과 같은 HTTP 서버 구현체들이 HTTP 상태 코드로 구현을 해버렸습니다.
그래서 처음에는 HTTP 의장이 프로젝트마다 돌아다니면서 418 지원을 제거해야 된다는 이슈를 올렸는데 맹비난을 받고 포기했습니다. 그래서 418은 HTTP 상태코드에서 영구 결번으로 만들게 되었습니다. 이미 그런 구현체가 존재하고, 그런 잘못 만든 구현체들과 상호 운용성을 지켜주기 위해(안그러면 웹이 깨지기 때문에) 이러한 결정을 한 것입니다.

또한 HTTP 버전이 올라가면서 예전 버전을 빼려고 시도했지만 몇몇 프록시에서 오작동 하는 것이 발견되어서 HTTP/0.9를 유지하게 되었습니다.

이렇게 많은 사람들이 상호 운용성에 대한 노력을 했고 덕분에 독립적으로 진화될 수 있었습니다.

실제로 REST가 웹의 독립적 진화에 도움을 주었는가

그렇습니다. 실제로 HTTP에 지속적으로 영향을 주었습니다. 이러한 영향을 준 것들은 다음과 같습니다.

  • Host 헤더 추가 : REST 아키텍처를 만들면서 필요성을 느껴서
  • 길이 제한 다루는 방법 명시 (414 URI Too Long 등)
  • URI에서 리소스의 정의가 추상적으로 변경 : "문서의 위치" → "식별하고자 하는 무언가"

이 외에도 기타 HTTP와 URI에 많은 영향을 주었습니다. 결국 최신 HTTP/1.1 명세에는 REST에 대한 언급이 들어가게 되었습니다.
representaion의 개념이 정의되어 있는데 "이 개념은 REST에서 온 것이다."라면서 박사학위 논문 링크가 걸려 있습니다.
사실 이렇게 된 것은 REST에 깊은 감명을 받은 것도 있지만, 로이 필딩이 REST를 만든 사람이자 HTTP와 URI 명세 저자 중 1명이기 때문입니다. 당연히 영향을 줄 수 밖에 없습니다.

그렇다면 REST는 성공 했는가?
REST는 웹의 독립적 진화를 위해 만들어졌습니다. 그리고 웹은 독립적으로 진화하고 있습니다. 그래서 REST는 성공했다고 말할 수 있습니다.

그런데 REST API는...?
REST API는 REST 아키텍처 스타일을 따라야 하는데, 오늘 날 스스로 REST API라고 하는 API들의 대부분이 REST 아키텍처 스타일을 따르지 않고있습니다.

REST API도 제약 조건들을 다 지켜야 하는건가?

몇 가지 빠트려도 되지 않을까...? 다 지켜야 되나? 그렇습니다!

"An API that provides network-based access to resources via uniform interface of self-descriptive messages containing hypertext to indicate potential state transition might be part of an overall system that is a RESTful application. - Roy T. Fielding

로이 필딩은 REST API라고 하는 것은 하이퍼 텍스트를 포함한 Self-descriptive 메시지의 uniform interface를 통해 리소스에 접근하는 API라고 강조했습니다. 즉, 모든 제약 조건을 지켜야한다고 했습니다.

이렇게 보니...REST는 꽤나 복잡하고 어려워 보입니다.

8) 원격 API가 꼭 REST API여야 하는건가?

우리가 만드는 원격 API가 꼭 REST API이어야 할까요? 이에 로이필딩은 아니여도 된다고 합니다.

"REST emphasizes evolvability sustain on uncontrollable system. If you think you have control over the system or aren't interested in evolvability, don't waste your time arguing about REST. - Roy T. Fielding
시스템 전체를 통제할 수 있다고 생각하거나, 진화에 관심이 없다면, REST에 대해 따지느라 시간을 허비하지 마라.

시스템 전체를 통제할 수 있는 경우는 회사에서 서버 개발자인데, 클라이언트 개발자를 통제 가능할 때 또는 혼자서 클라이언트, 서버를 다 만들 수 있을 때를 말합니다. 이런 경우에는 굳이 따질 필요가 없다는 말입니다.
진화에 관심이 없다는 것은 매일 업데이트 해서 유저들에게 불만을 들어도 상관이 없다는 것입니다.

하지만 우리는 시스템 전체를 통제할 수 없기도 하고, 개발자로서 진화에 관심을 두어야 하기에 REST를 따라야 합니다.

9) 그럼 이제 어떻게 해야되는가?

그렇다면 이제 어떻게 해야할까요? 우리가 선택할 수 있는 선택지는 세 가지가 있습니다.

  1. REST API를 구현하고 REST API라고 부른다.
  2. REST API 구현을 포기하고 HTTP API라고 부른다.
  3. REST API가 아니지만 REST API라고 부른다. (현재 상태)

현재 상태는 엄밀히 말라면 REST API가 아니지만 REST API라고 부르고 있습니다. 그러면 로이 필딩이 이렇게 말합니다.

"I am getting frustrated by the number of people calling any HTTP-base interface a REST API... Please try to adhere to them or choose some other buzzword for your API." - Roy T. Fielding
제발 제약 조건을 따르던지 아니면 다른 단어를 써라

10) 진짜 REST API를 구현하자

그러면 진짜 REST API를 구현하는 것에 도전하기 위해서 한 번 알아보겠습니다.

1. 왜 API는 REST가 잘 안되나?

먼저 왜 API는 REST가 잘 안되는지 알아보겠습니다. REST가 잘 지켜지는 웹하고 비교해보겠습니다.

null

표에서 나온 것 처럼 커뮤니케이션이 조금 다릅니다. 웹 페이지는 사람과 기계 간의 커뮤니케이션인데 HTTP API는 사람이 아닌 기계가 해석을 합니다. 그러다 보니 미디어 타입이 다릅니다. HTTP API의 경우 Json이나 XML같이 기계가 의미를 이해할 수 있는 포맷을 쓰게 됩니다.

그렇다면 문제의 원인이 미디어 타입이라는 생각이 들게 됩니다. HTML과 JSON을 비교해보겠습니다.

null

Hyperlink는 a 태그가 있기 때문에 HTML은 잘 됩니다. JSON은 그런게 없습니다.

Self-descriptive 측면에서 보면 HTML은 명세가 있습니다. 명세를 보면 모든 태그들에 대한 정의가 되어 있습니다.
반면에 JSON은 불완전합니다. 불완전 하다는 것은 적어도 문법은 정의되어 있다는 뜻입니다. 어떻게 파싱하고 array를 어떻게 해석해라 까지는 되어있지만 안에 들어갈 수 있는 key-value에 대한 의미는 아무 것도 정의되어 있지 않습니다.
따라서 문법은 해석 가능하지만 의미를 해석하려면 별도로 문서(API 문서 등)가 필요합니다.

HTML과 JSON을 코드로 비교해보겠습니다.

HTML

GET /todos HTTP/1.1  
Host: example.org

HTTP/1.1 200 OK  
Content-Type: text/html

<html>
<body>
<a href="https://todos/1">회사 가기</a>
<a href="https://todos/2">집에 가기</a>
</body>
</html>

HTML이 Self-descriptive한 지 따져보겠습니다.

  1. 응답 메시지의 Content-Type을 보고 미디어 타입이 text/html임을 확인했습니다.
  2. HTTP 명세에 미디어 타입은 IANA에 등록되어 있다고 하므로, IANA에서 text/html의 설명을 찾습니다.
  3. IANA에 따르면 text/html 명세는 http://www.w3.org/TR/html 이므로 링크를 찾아가 명세를 해석합니다.
  4. 명세에 모든 태그의 해석 방법이 구체적으로 나와있으므로 이를 해석하여 문서 저자가 사용자에게 보여줄 수 있습니다.

따라서 Self-descriptive하다고 할 수 있습니다.

HATEOAS도 만족하는지 확인해보면 a 태그를 이용해 표현된 링크를 통해 다음 상태로 전이될 수 있으므로 만족합니다.

JSON

GET /todos HTTP/1.1  
Host: example.org

HTTP/1.1 200 OK  
Content-Type: application/json

[  
    {"id": 1, "title": "회사 가기"},  
    {"id": 2, "title": "집에 가기"}  
]

JSON이 Self-descriptive한 지 확인해보겠습니다.

  1. 응답 메시지의 Content-Type을 보고 미디어 타입이 application/json임을 확인했습니다.
  2. HTTP 명세에 미디어 타입은 IANA에 등록되어 있다고 하므로, IANA에서 application/json의 설명을 찾습니다.
  3. IANA에 따르면 application/json 명세는 draft-ietf-jsonvis-rfc7159bis-04 이므로 링크를 찾아가 명세를 해석합니다
  4. 명세에 JSON 문서 파싱 방법이 명시되어 있으므로 파싱을 성공합니다. 그러나 "id"가 무엇을 의미하고, "title"이 무엇을 의미하는지 알 방법 없습니다.

따라서 Self-decriptive하다고 할 수 없습니다.

HATEOAS를 만족하는지 확인해보면 다음 상태로 전이할 링크가 없기 때문에 역시 실패하게 됩니다.

2. 그런데 Self-descriptive, HATEOAS가 어떻게 독립적인 진화에 도움이 되는가?

이렇게 확인을 하다보니 의문이 생깁니다. Self-descriptive, HATEOAS는 정말 독립적인 진화에 도움이 되는 것이 맞을까요?

Self-descriptive는 확장 가능한 커뮤니케이션이 가능하도록 합니다. 서버나 클라이언트가 변경되더라도 오고가는 메시지는 언제나 Self-descriptive 하므로 언제나 해석이 가능합니다.

HATEOAS는 애플리케이션 상태 전이의 late binding이 가능하도록 한다고 합니다. 어디서 어디로 전이가 가능한지 미리 결정되지 않고, 어떤 상태로 전이가 완료 되어야 그 다음 전이될 수 있는 상태가 결정된다는 것인데 쉽게 말해서 링크가 동적으로 변경될 수 있다는 말입니다.

3. 그럼 REST API로 고쳐보자

먼저 Self-descriptive에 몇 가지 방법이 있는데 먼저 Media type 정의가 있습니다.

  1. 미디어 타입을 하나 정의
  2. 미디어 타입 문서 작성 → "id", "title"의 의미 정의
  3. IANA에 미디어 타입을 등록. 이때 만든 문서를 미디어 타입의 명세로 등록

이렇게 하면 이제 이 메시지를 보는 사람은 명세를 찾아갈 수 있습니다. 메세지의 의미를 온전히 해석할 수 있습니다.

GET /todos HTTP/1.1  
Host: example.org

HTTP/1.1 200 OK  
Content-Type: application/vnd.todos+json

[  
    {"id": 1, "title": "회사 가기"},  
    {"id": 2, "title": "집에 가기"}  
]

이 방법의 단점은 상당히 번거롭다는 것입니다. 매번 media type을 정의해야 합니다.

그 다음 조금 괜찮은 방법은 Profile을 이용하는 것입니다.

  1. "id", "title" 의미 정의한 명세 작성
  2. Link 헤더에 profile relation으로 명세 링크
GET /todos HTTP/1.1  
Host: example.org

HTTP/1.1 200 OK  
Content-Type: application/json  
Link: <https://example.org/docs/todos>; rel="profile"

[  
    {"id": 1, "title": "회사 가기"},  
    {"id": 2, "title": "집에 가기"}  
]

단점은 클라이언트가 Link 헤더(RFC5988)와 profile(RFC 6906)을 이해해야 합니다. 그리고 Content negotiation을 할 수 없습니다.

HATEOAS를 만족시키는 방법 중 흔한 방법은 본문에 링크를 박아넣는 것입니다. data에 다양한 방법으로 하이퍼링크를 표현하는 방법입니다.

GET /todos HTTP/1.1  
Host: example.org

HTTP/1.1 200 OK  
Content-Type: application/json  
Link: <https://example.org/docs/todos>; rel="profile"

[  
    {  
        "link": "https://example.org/todos/1",  
        "title": "회사 가기"  
    },  
    {  
        "link": "https://example.org/todos/2", 
        "title": "회사 가기"  
    }
]
GET /todos HTTP/1.1  
Host: example.org

HTTP/1.1 200 OK  
Content-Type: application/json  
Link: <https://example.org/docs/todos>; rel="profile"

{  
    "links" : {  
        "todo" : "https://example.org/todos/{id}"
    },  
    "data": [{  
        "id": 1,  
        "title": "회사 가기"  
        }, {  
        "id": 2,  
        "title": "집에 가기"  
    }]  
}

단점으로는 링크를 표현하는 방법을 직접 정의해야 합니다.

JSON으로 하이퍼링크를 표현하는 방법을 정의한 명세들을 활용하는 방법도 있습니다. JSON API, HAL, UBER, Siren 등등...

GET /todos HTTP/1.1  
Host: example.org

HTTP/1.1 200 OK  
Content-Type: application/vnd.api+json  
Link: <https://example.org/docs/todos>; rel="profile"

{  
    "data": [{  
        "type": "todo",  
        "id": 1,  
        "attributes": {"title": "회사 가기"},  
        "links" : { "self": "https://example.org/todos/1" }
    }, {  
        "type": "todo",  
        "id": 2,  
        "attributes": {"title": "집에 가기"},  
        "links" : { "self": "https://example.org/todos/2" }  
    }]  
}

이 방법은 기존 API를 많이 고쳐야한다는 단점이 있습니다.

그 다음으로 Link, Location 등의 HTTP 헤더로 링크를 표현하는 방법이 있습니다.

POST /todos HTTP/1.1  
Content-Type: application/json

{  
    "title": "점심 약속"  
}

HTTP/1.1 204 No Content  
Location: /todos/1  
Link: </todos/>, rel="collection"  

결국 HATEOAS의 경우 data와 헤더 모두 활용해서 표현하는 것이 좋습니다.

그렇다면 몇 가지 궁금점이 생기는데 먼저 Hyperlink는 반드시 uri여야 하는 것일까요? 그렇지는 않습니다. 상태 uri나 템플릿도 상관없이 hyperlink인 것만 표현이 되면 됩니다.
그러면 Media type 등록은 필수인가요? 이에 대해 로이 필딩은 다음과 같이 말합니다.

"A REST API should be entered with no prior knowledge beyond the initial URI (bookmark) and set of standardized media types that are appropriate for the intended audience(i.e., expected to be understood by any client that might user the API). — Roy. T. Fielding
의도한 저자가 이해할 수만 있다면 상관 없다. 예를 들면 사내에서만 쓰는 API고 이를 이해하고 있다면 굳이 등록하지 않아도 된다.

물론 IANA 등록의 장점도 있습니다. 사전에 알고 있지 않은 사람도 누구나 쉽게 사용할 수 있게 됩니다. 또한 이름 충돌을 피할 수 있다고 합니다.
그리고 "등록이 별로 어렵지 않다"고 주장합니다.

3. 정리

오늘 날 대부분 "REST API"는 사실 REST를 따르고 있지 않습니다. 왜냐하면 REST의 제약 조건 중 특히 Self-descriptive와 HATEOAS를 잘 만족하지 못하고 있기 때문입니다.

REST는 긴 시간에 걸쳐(수십년) 진화하는 웹 애플리케이션을 위한 것입니다. 따라서 REST를 따를 것인지는 API 설계하는 이들이 스스로 판단하여 결정해야 합니다.

REST를 따르겠다면 Self-decriptive와 HATEOAS를 만족시켜야 합니다.
Self-decriptive는 custom media type, profile link relation 등으로 만족시킬 수 있고, HATEOAS는 HTTP 헤더나 본문에 링크를 담아 만족시킬 수 있습니다.

REST를 따르지 않겠다면 "REST를 만족하지 않는 REST API"를 뭐라고 부를지 결정해야 할 것입니다.
HTTP API라고 부를 수도 있고 그냥 이대로 REST API라고 부를 수도 있습니다.로이 필딩이 싫어하겠지만

마이크로 소프트 같은 경우에도 REST를 HTTP로 고치는 Pull Request를 무시했습니다. 어떻게든 스스로 결정내릴 수만 있으면 괜찮습니다.

댓글