restAPI 정확하게 설계하기(feat.그런 REST API로 괜찮은가)
restAPI 정확하게 설계하기(feat.그런 REST API로 괜찮은가)
개요
사이트 프로젝트로 restAPI를 개발하면서 restAPI란 무엇인가에 대해 공부하다가 "그런 REST API로 괜찮은가" 라는 영상을 보게 되었다.
이 영상을 통해 내 restAPI도 REST를 만족하지 못하고있었구나 하고 깨닫게 되어 restAPI 정확하게 설계하기위해 위 영상을 정리하고자한다.
REST API
REST API → REST 아키텍처를 따르는 API
REST → 분산 하이퍼미디어 시스템(ex. 웹)을 위한 아키텍처 스타일
아키텍처 스타일 → 제약 조건의 집합
즉, REST에서 정의한 제약 조건을 모두 지켜야 REST를 따른다고 말할 수 있다는 것이다.
사실 REST는 아키텍처 스타일이면서 하이브리드 아키텍처 스타일이라고 말한다. 왜냐하면 아키텍처 스타일이면서, 동시에 아키텍처 스타일의 집합이기 때문이다.
REST를 구성하는 제약조건(아키텍쳐 스타일)
- Client-Server
- Stateless
- Cache
- *Uniform Interface
- Layered System
- Code-on-Demand (optional)
대체로 REST라고 부르는 것들은 위의 조건을 대부분 지키고 있다.
왜냐하면 HTTP만 잘 따라도 Client-Server, Stateless, Cache, Layered System은 다 지킬 수 있기 때문이다.
Code-on-Demand는 서버에서 코드를 클라이언트로 보내서 실행할 수 있어야 한다는 것을 의미, 즉 자바스크립트를 의미한다. 이는 필수는 아니다.
하지만 로이가 REST가 아니라고 말하는 것들은 Uniform Interface를 만족하지 못하는 것이 대부분이다. Uniform Interface 역시 아키텍처 스타일이기 때문에 안에 뭐가 들어있는지 보자.
Uniform Interface 을 왜 만족하지 못할까?
Uniform Interface 제약 조건 4 가지
Identification of resources :
URI로 리소스가 식별되어야 한다.
Manipulation of resources through representations
리소스를 만들거나 삭제, 수정할 때 http 메시지에 그 표현을 전송해야된다.
- Self-descriptive messages
- Hypermedia as the engine of application state(HATEOAS라고 부른다)
문제는 Self-descriptive messages 와 HATEOAS 2개이다.
이 2가지는 사실 우리가 REST API라고 부르는 거의 모든 API들이 지키지 못하고 있다.
Self-descriptive messages 와 HATEOAS가 뭔지 알아보자.
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 헤더에서 대괄호, 중괄호, 큰따옴표의 의미가 뭔지 알게 되어, 파싱이 가능하여 문법을 해석할 수 있게 된다.
그렇다면 이제 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이라고만 되어있고 이를 어떻게 해석해야되는지는 명시가 되어있지 않다.
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 헤더가 이미 표준으로 문서가 나와 있기 때문에 이 메시지를 보낸 사람이 온전히 해석해서 어떻게 링크가 되어 있는가를 이해하고 하이퍼링크를 타고 다른 상태로 전이가 가능하다.
굳이 Uniform Interface까지 지켜야될까?
- 당연히 지켜야한다. REST를 만든 본질적인 목적에서 그 이유를 찾을수있다.
독립적 진화
- 서버와 클라이언트가 각각 독립적으로 진화한다.
- 서버의 기능이 변경되어도 클라이언트를 업데이트할 필요가 없다.
- REST를 만든 계기 : "How do I imporve HTTP without breaking the Web"
서버가 변경(새로운 API가 추가되고 기존 API가 변경/삭제되고 URI가 바뀜)되어도 클라이언트는 바꿀 필요가 없어야한다.
이게 바로 REST를 만들게 된 계기이다
REST가 목적하는 바가 독립적인 진화이다.
이를 달성하기 위해서는 Unifrom Interface가 필수적이기기에, 이를 만족하지 못하면 REST라고 부를 수 없는 것이다.
self-descriptive, HATEOAS에 따라 모든 메시지는 스스로를 설명하고 하이퍼링크를 통해 다음 상태로 전이할수있어야한다.
self-descriptive, HATEOAS가 어떻게 독립적인 진화에 도움이 되는가?
self-descriptive → 확장 가능한 커뮤니케이션
서버나 클라이언트가 변경되더라도 오고가는 메시지는 언제나 self-descriptive 하므로 언제나 해석이 가능하다.
HATEOAS → 애플리케이션 상태 전이의 late binding
어디서 어디로 전이가 가능한지 미리 결정X. 어떤 상태로 전이가 완료 되어야, 그 다음 전이될 수 있는 상태 결정 ⇒ 링크는 동적으로 변경될 수 있다.
REST가 웹의 독립적 진화에 도움을 주었다.
HTTP에 지속적으로 영향을 주었다.
- Host 헤더 추가
- 길이 제한 다루는 방법 명시 (414 URI Too Long 등)
- URI에서 리소스의 정의가 추상적으로 변경: "문서의 위치" → "식별하고자 하는 무언가"
- HTTP/1.1 명세 최신판에서 REST에 대한 언급이 들어감
그럼 REST는 성공 했는가?
REST는 웹의 독립적 진화를 위해 만들어졌다.
웹은 독립적으로 진화하고 있다. → 성공!
REST API 정확하게 설계하기
왜 API는 REST가 잘 안되나?
웹하고 비교해보자. 일반적인 MVC와 API의 차이점이다.
커뮤니케이션이 좀 다르다 HTTP API는 사람이 아닌 기계가 해석한다. 그러다 보니 미디어 타입이 다르다. Json이나 XML같이 기계가 의미를 이해할 수 있는 포맷을 쓰게 된다.
그렇다면 문제의 원인이 미디어 타입이라고 생각이 된다.
self-descriptive 측면에서 보면 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>
<head> </head>
<body>
<a href="https://todos/1"> 회사 가기 </a>
<a href="https://todos/2"> 집에 가기 </a>
</body>
</html>
HTML → Self-descriptive
- 응답 메시지의 Content-Type을 보고 미디어 타입이 text/html 확인
- HTTP 명세에 미디어 타입은 IANA에 등록되어 있다고 함 → IANA에서 text/html 명세 찾음
- IANA에 따르면 text/html 명세는 http://www.w3.org/TR/html 이므로 링크를 찾아가 명세 해석
- 명세에 모든 태그의 해석 방법이 있으므로 이를 해석, 사용자에게 보여줄 수 있음
- Success
HTML → HATEOAS
- a 태그를 이용해 표현된 링크를 통해 다음 상태로 전이될 수 있으므로 만족
- Success
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
- 응답 메시지의 Content-Type을 보고 미디어 타입이 application/json 확인
- HTTP 명세에 미디어 타입은 IANA에 등록되어 있다고 함 → IANA에서 application/json 명세 찾음
- IANA에 따르면 application/json 명세는 draft-ietf-jsonvis-rfc7159bis-04 이므로 링크를 찾아가 명세 해석
- 명세에 JSON 문서 파싱 방법이 명시 → 성공적으로 파싱, 그러나 "id"가 무엇을 의미하고, "title"이 무엇을 의미하는지 알 방법 없음.
- FAIL
JSON → HATEOAS
- 다음 상태로 전이할 링크가 없다.
- FAIL
그럼 REST API로 바꿔보자
- 여러가지 방법들이 있지만 개인적으로 활용할만한 방법을 정리하겠다.
Self-descriptive 해결
Profile 이용
"id", "title" 의미 정의한 명세 작성
Link 헤더에 profile relation으로 명세 링크
이제 메시지를 보는 사람은 명세를 찾아갈 수 있음 → 의미 해석 가능
GET /todos HTTP/1.1 Host: example.org HTTP/1.1 200 OK Content-Type: application/json Link: <https://exmaple.org/docs/todos>; rel="profile" [ {"id": 1, "title": "회사 가기"}, {"id": 2, "title": "집에 가기"} ]
HATEOAS 해결법
- 응답 data에 다양한 방법으로 하이퍼링크 표현
혹은
HTTP 헤더로 → Link, Location 등
GET /todos HTTP/1.1 Host: example.org HTTP/1.1 200 OK Content-Type: application/json Link: <https://exmaple.org/docs/todos>; rel="profile" Location: /todos/1 Link: </todos/>, rel="collection" { "links" : { "todo" : "https://example.org/todos/{id}" <- 다음 전이 하이퍼 링크 }, "data": [{ "id": 1, "title": "회사 가기" }, { "id": 2, "title": "집에 가기" }] }
body나 헤더에 다음 상태 전이 하이퍼링크를 첨부
정리
- 오늘날 대부분 "REST API"는 사실 REST를 따르고 있지 않다.
- REST의 제약 조건 중 특히 Uniform Interface의 self-descriptive와 HATEOAS를 잘 만족하지 못한다.
나머지들은 HTTP를 지키기만해도 만족한다.
- REST를 따르겠다면 Self-decriptive와 HATEOAS를 만족시켜야한다
- 모든 서버가 restAPI일 필요는 없다.
개발자의 선택이다. Graphql등 다양한 방식들이 존재한다.
하지만 가장 대중적이고 널리 쓰이는 아키텍쳐는 REST가 아닌가 싶다.
Reference
https://www.youtube.com/watch?v=RP_f5dMoHFc