감상문

그런 REST API로 괜찮은가

ihl 2021. 2. 6. 18:09

youtu.be/RP_f5dMoHFc

 

REST API란 정확히 무엇을 의미하는 것일까? 나는 REST API를 URI를 통해 리소스를 식별하고 GET, POST, DELETE 등의 메소드로 동작을 처리하는 구조 정도로만 이해하고 있었다. 위 영상을 통해 REST API가 무엇인지, 사용 목적이 무엇인지 구체적으로 이해할 수 있었다.


REST API란 REST 아키텍처 스타일을 따르는 API이다. REST 스타일은 Client-server, stateless, cache, uniform interface, layered system, code-on,demand(optional)로 구성이 되어 있으며 이들을 모두 만족해야 REST라고 표현할 수 있다. 그런데 자칭 REST API라 하는 많은 API들이 이들을 모두 만족하지 않으면서 REST API라고 주장하고 있다.

 

많은 자칭 REST API들이 REST 스타일 중 Uniform Interface라는 제약 조건을 따르지 않는다. Uniform Interface의 제약 조건은 다음과 같다.

 

  • Identification of resources: 리소스가 URI로 식별
  • Manipulation of resources through representation: 리소스를 처리할 때 URI의 메세지에 해당 내용이 포함
  • self-descriptive messages: 메시지는 스스로를 설명해야함
  • hypermedia as the engine of application state(HATEOAS): 애플리케이션의 상태는 Hyperlinke를 통해 전이됨

이 중 3, 4번째 조건인 self-descriptive와 HATEOAS를 대부분 지키지 못하고 있다. 이 것이 어떤 것을 의미하는지 알아보자.

 

*Self-Descriptive

//1번 응답 -> [], {} 의 의미 알 수 없음
HTTP/1.1 200 OK
[{"op": "remove", "path": "/ihl/trash"}]

//2번 응답 -> op, path의 의미 알 수 없음
HTTP/1.1 200 OK
Content-Type: application/json
[{"op": "remove", "path": "/ihl/trash"}]

//3번 응답 -> Self descriptive 만족!
HTTP/1.1 200 OK
Content-Type: application/json-patch+json
[{"op": "remove", "path": "/ihl/trash"}]

Self-descriptive message란 메시지의 내용 자체만으로도 온전한 해석이 가능해야한다는 것이다. 위 3가지 응답 메시지에서 1번과 2번은 self-descriptive를 만족하지 않는다. (보통은 추가적인 API문서를 따로 찾아봐야한다.) 3번 처럼 구체적으로 미디어 타입을 등록하여야 메시지를 본 사람이 미디어 타입 명세를 보고 메시지의 의미를 해석할 수 있다. 

 

*HATEOAS

HATEOS란 애플리케이션의 상태는 Hyper link를 이용해 전이되어야 하는 것이다. HTML의 경우 대부분 이를 만족하지만 JSON 메시지는 이를 만족하지 못하는 경우가 대다수이다.

 

Uniform Interface

왜 Self-Descriptive와 HATEOAS를 지켜야하는 것일까? 바로 독립적으로 진화하기 위해서이다. 이는 서버의 기능이 변경되어도 클라이언트를 업데이트할 필요가 없다는 의미와 동일하다. REST를 잘 지켜서 독립적 진화가 가능한 대표적인 예가 웹이다. 웹 페이지가 변경했다고 웹 브라우저를 업데이트할 필요가 없으며 웹 브라우저를 업데이트했다고 웹 페이지를 변경할 필요가 없다. 브라우저에 따라 UI가 조금 깨질 수는 있지만 동작에는 전혀 지장이 없다.

 

 Self-descriptive의 경우 서버나 클라이언트가 변경되어도 오고가는 메시지는 스스로를 잘 설명하고 있기 때문에 언제가 해석이 가능해진다. HATEOS의 경우 애플리케이션 상태 전이가 나중에 결정(late binding)되므로 링크가 동적으로 변경할 수 있다. 따라서 서버가 링크를 바꾸면 클라이언트는 바뀐 링크를 보고 그대로 따라가기만 하면 된다.

 

REST API화

API를 지속적으로 업데이트 할 예정이라면 REST API를 지켜주는 것이 상호 운용성(interoperability)을 위해 좋은 방향일 것이다. 그렇다면 Self-descriptive와 HATEOAS를 지키기 위해 JSON 메시지를 어떤 방식으로 구현하면 될지 알아보자.

 

//1. MediaType
GET /IHLScores HTTP/1.1
Host: ihl.com

HTTP/1.1 200 OK
Content-Type: application/vnd.IHLScores+json
[{"id": "수학", "Score": 88}, {"id": "영어", "Score": 87}]


//2. Profile
GET /IHLScores HTTP/1.1
Host: ihl.com

HTTP/1.1 200 OK
Content-Type: application/json
Link: <https://ihl.com/docs/IHLScores>; rel="profile"
[{"id": "수학", "Score": 88}, {"id": "영어", "Score": 87}]

Self-description의 경우 2가지 방법이 있다. 첫 번째는 IANA에 내가 사용할 Media Type을 등록하는 것이다. 이 방법의 단점은 매번 Media Type을 정의해야한다는 점이다. 두 번째는 Profile을 사용하는 것이다. 이 경우 클라이언트가 Link와 profile을 이해할 수 있어야 한다는 조건이 생긴다. 또한 Content negotiation이 불가능한 단점이 있다.(정확히는 이해하지 못했지만 Client가 요구스펙을 만족하지 못할 때 Server가 알 수 없다. 는 의미인 것 같다)

 

//방법1-1. 직접 여러가지 방식으로 하이퍼링크를 넣는다.
//예시1
GET /IHLScores HTTP/1.1
Host: ihl.com

HTTP/1.1 200 OK
Content-Type: application/json
Link: <https://ihl.com/docs/IHLScores>; rel="profile"
[
  {
   "link": "https://ihl.com/id/1", 
   "Score": 88
   }, 
  {
   "link": "https://ihl.com/id/2", 
   "Score": 87
   }
]

//예시2
GET /IHLScores HTTP/1.1
Host: ihl.com

HTTP/1.1 200 OK
Content-Type: application/json
Link: <https://ihl.com/docs/IHLScores>; rel="profile"
{
  "links": {"Score": "https://ihl.com/id/{id}"},
  "data": [{"id": "수학", "Score": 88}, {"id": "영어", "Score": 87}]
}

//방법2. 헤더에 링크를 넣는다.
HTTP/1.1 204 No Content
Location: /id/1
Link: </id/>; rel="collection"

HATEOAS를 만족시키는 첫 번째 방법은 메시지에 직접 하이퍼링크를 넣는 것이다. 위와 같은 여러 방식으로 하이퍼링크를 객체에 포함시키면 된다. 혹은 JSON으로 하이퍼링크를 표현하는 방법을 정의한 JSON API, HAL, UBER, Siren 등의 명세를 활용하여 하이퍼링크를 표현할 수 있다. 두 번째 방법은 Link, Location 등의 헤더를 이용하여 링크를 표현하는 것이다.


이 외에 내가 추가적으로 공부한 부분은 리소스를 나타내는 방식이다. 리소스를 나타낼 때 다음과 같은 규칙을 지켜야한다.

 

1. 리소스를 나타내기 위해 명사를 사용하라

//1. document
/user-management/users/{id}

//2. collection
/user-managements/users/{id}/accounts

//3. store
/cart-management/users/{id}/carts

//4. controller
/cart-management/users/{id}/cart/checkout

공식문서에서 리소스는 크게 document/collection/store/controller로 분류한다. 각 분류에 다라 위와 같이 이름을 지어야 한다.(예를들어 collection은 복수형을 사용한다...) 이 때 CRUD와 같은 동작 이름 등을 URI에 사용하지 않아야 한다. 

 

2. 일관적인 이름을 지어라

다음과 같은 규칙을 지켜 일관적인 이름을 짓는 것이 좋다.

  • 계층구조를 나타낼 때 /사용하기
  • URI 끝에 / 붙이지 않기
  • URI의 가독성을 높이기 위해 -사용
  • _는 사용하지 않음
  • URI에 소문자 사용
  • 파일확장자는 사용하지 않음

3. filterring해야한다면 query component를 사용하라

/user-management/users?id=32&&region=USA

리소스를 필터링해야할 때가 있다. 이 때는 위와 같이 query component를 활용하자.


사실 위의 사항을 지키지 않아도 웹을 당장 서비스하는 것에는 문제가 없기 때문에 지나쳐가기 쉬운 부분인 것 같다. 그러나 API는 한 번만들고 끝이 아니라 끊임 없이 수정되야 하므로 힘들고 낯설더라도 REST API로 충족시키는 방향으로 가야한다고 생각이 들었다.  Media Type, Profile, Header 등의 사용 예시들을 더 찾아보고 활용할 수 있도록 연습해봐야겠다.