REST API는 없다

2022-05-24


내가 아는 REST API

프론트엔드와 백엔드가 함께 일하다보면, 통신을 주고받는 과정에서 피할 수 없는 개념이 있다. 바로 REST API이다. API를 만들 때 항상 REST API로 만들어달라고 하거나 만들어 달라고 요구하는 경험이 꼭 있을 것이다. 그래서 REST API에 대해서 정리를 해보면서 어떤게 REST API인지 알아보고자 한다.

REST API는 정말 간단하다. API 서버를 만들 때 아래처럼 구현하면 된다.

자원 목록(collection)과 자원 하나(item)이라는 개념에 접근할 수 있는 경로(식별자)와 이 자원에 하려는 행동(CRUD)을 나눠서 요청할 수 있도록 만들면 된다.

위의 요구사항대로 API를 만들면 보통 6개의 API를 만들게 된다.

# items라는 자원의 **목록 전체를 호출**한다.
curl -X GET /items

# items라는 자원에서 :itemId에 해당하는 **자원 하나를 호출**한다.
curl -X GET /items/:itemId

# items라는 자원에 데이터의 내용이 포함된 **자원을 생성**한다.
curl -X POST /items -d '{ "name": "new item", "content": "new item is created!" }'

# items라는 자원 중 :itemId와 일치하는 **자원의 내용을 대체**한다.(대체와 일부 수정에서 PATCH와 미묘한 차이가 있다.)
curl -X PUT /items/:itemId -d '{ "name": "new item", "content": "new item is replaced!" }'

# items라는 자원중 :itemId와 일치하는 **자원의 내용을 일부 수정**한다.
curl -X PATCH /items/:itemId -d '{ "content": "new item is updated partially!" }'

# items라는 자원중 :itemId와 일치하는 **자원을 삭제**한다.
curl -X DELETE /items/:itemId

사용된 API 호출을 추상화를 해보자면 {자원에 할 행동} {자원에 대한 식별자}으로 어떤 자원에 어떤 행동을 할 것인지를 명시하는 방식으로 요청이 설계되어 있다. 이렇게 하면 API를 사용하는 사용자(프론트엔드 개발자)는 API에 대한 이해가 엄청 빨라진다.

특정 버튼을 클릭했을 때 하나의 자원 추가가 필요하다면, POST /items 으로 호출하면 당연히 생성이 되는거다.

자원의 "목록"과 자원 "하나"라는 차이를 이해하면 언제 복수형으로 사용하고, 언제 복수형에 id를 포함해서 호출해야하는지를 판단할 수 있고, 특정 행동(자원 생성과 자원 목록 호출)은 자원 목록에만 사용해야한다는 것을 이해할 수 있다.

여기에 접근하는 자원의 경로(식별자)와 행동에 따라 정해진 형식의 응답 모양을 주면 프론트엔드 개발자는 API에 대한 예측이 쉬워져서 API에 대한 이해를 위한 리소스를 줄이고 오직 개발에만 집중할 수 있게 된다. 이 것은 개발 효율에서도 엄청난 장점이라고 볼 수 있다.

핵심은 필요한 자원과 행동을 전달하면 그에 맞는 응답을 준다이고 이 아키텍쳐는 그에 걸맞는 아주 섹시한 API라는 생각이든다.

여기까지가 내가 알고 있는 REST API의 전부였다.

계기

REST 라는 느낌은 알지만 의심 없이 사용하던 시절 전 직장에서 면접에 참여하게 되는 상황이 있었는데, 면접자가 작성해온 서버 코드에서 응답 코드는 200만 사용하고, 응답 내용에 statusCode: 400 라는 내용이 들어가는 경우를 보았다. 일반적이지 않은 표현이라 위화감을 느끼고 API를 만드는 방법에 대해서 찾아보게 되었다. HTTP API라는 것도 WEB API라는 것도 있었는 데, 내가 알고 사용하는 REST API의 모양과 크게 다르지 않다는 것을 알았다.

엥? 그럼 REST API는 다른 API랑 정확히 뭐가 다른거지? 라는 생각이 들었다.

REST API는 정확하게 무엇인가?

REST API는 "REST 아키텍쳐를 따르는 API"라고 한다.

이게 무슨 뜻이냐? 일단 익숙한 개념부터 분리해서 알아 볼 필요가 있다. REST는 처음 보는 단어인데 API는 익숙하다. API부터 알아봐야겠다. API는 위키백과에 따르면 application programming interface(API)라고 한다. 좀 더 풀어서 말하면 컴퓨터와 컴퓨터나 컴퓨터 내의 프로그램들 사이에서 서로를 호출하는 방법이다. 더 쉽게 비유하자면 프론트엔드(브라우저 프로그램)에서 백엔드(서버 컴퓨터)를 호출하는 방법이라고 볼 수 있다. 아 그러면 API라는 것은 프론트가 서버를 호출하는 방법이고, REST API는 "REST 아키텍쳐를 따르는 서버를 호출하는 방법"이구나 까지 이해할 수 있다. 아직 REST는 뭔지 모르겠지만, API가 뭔지는 이제 정확히 이해했다.

REST는 REpresentational State Transfer의 줄임말이다. 직역하면 대표 상태 전이(?)라는 뜻인데 이 것을 어떻게 설명하고 이해할 수 있을까? 누가 무슨 생각으로 만든 것인지를 알아보면 좋을 것 같다는 생각이 들어 자료를 찾아보게 되었다.

REST는 누가 고안해냈나?

2000년에 Roy T Fielding이라는 미국의 훌륭한 컴퓨터 과학자가 박사 논문 'Architectural Styles and the Design of Network-based Software Architectures(아키텍쳐 스타일과 네트워크 기반 소프트웨어 아키텍쳐의 디자인)'을 제출하면서 처음으로 REST 이키텩쳐가 세상에 나왔다. 초록을 보면 대충 웹은 발전했고, 발전하면서 바뀌어버린 웹에 맞춰서 서비스의 변경과 배포 등에 더 유리한 새로운 아키텍쳐를 제안한다고 쓰여있다.

아래의 REST API에 대한 내용은 논문에서 내용을 발췌하였고, 해석하는 과정에서 얕은 영어 이슈로 잘못 이해했을 수도 있다.

진짜 REST API는 어떻게 만드는가?

REST는 몇가지(?)의 제약 조건만 맞으면 바로 만들 수 있다. 소프트웨어의 제약 조건을 Null에서 부터 하나씩 추가한다.(Starting with the Null Style: 단순한 빈 제약 조건 집합. 아키텍쳐 관점에서 null 스타일은 구성요소 간에 구별 되는 경계가 없는 시스템이다.)

REST의 제약조건

  1. Client-Server

    사용자의 인터페이스와 데이터 스토리지의 관심사를 분리하는 것이다. 이는 서버 구성요소를 단순화하여 확장성을 개선할 수 있다.

  2. Stateless

    Client-Server 통신은 본질적으로 상태가 없는 통신이어야한다. 클라이언트에서 서버로 보내는 모든 요청은 요청을 이해하는데 필요한 모든 정보가 포함되어야 하고, 서버에 저장된 context를 이용할수 없다. Session 상태는 전적으로 클라이언트에서 유지한다. 이 조건은 3가지 장점이 있다.

    • 가시성(Visibility): 모니터링 시스템이 하나의 요청을 검증하기 위해 이 요청까지의 히스토리를 볼필요가 없기 때문에 가시성이 좋아진다.(request 하나에서 모든 정보를 알 수 있기에 가시성이 좋다고 표현한 것 같다.)
    • 안정성(Reliability): 부분적 장애로 부터 복구하는 작업을 용이하게 하기 때문에 안정성이 향상된다.(중간에 기능 수정 배포가 되었을 때, 세션이 초기화되지 않아 잦은 서버 배포가 부담이 되지 않는다는 의미로 보인다.)
    • 확장성(Scalability): 상태 저장이 필요 없어서 서버 컴포넌트의 리소스 해제가 빠르기 때문에 서버 요청 간의 리소스 사용을 관리할 필요가 없기 때문에 구현이 더 단순해져서 확장성이 향상된다.

    웹 브라우저의 경우 쿠키에 로그인 정보를 담아서 저장해두는 방식으로 해결할 수 있는 내용이다.

  3. Cache

    네트워크 사용의 효율성을 위해 캐싱이 있어야한다. 이 제약 조건을 맞추려면 응답내에 캐싱 가능 여부에 대한 레이블이 필요하다. 캐싱 가능한 응답이라면 client side에서 캐싱을 해서 재사용한다.

    Cache-Control 이라는 헤더를 사용하면 브라우저에 캐싱이 가능하니까 많이들 사용하는 기능이다!

  4. Uniform Interface 자원에 대한 요청과 응답을 균일한 인터페이스로 만드는 것! 서비스를 구성하는 여러 컴포넌츠의 균일한 인터페이스가 다른 네트워크 베이스 아키텍쳐와 가장 큰 차이다.

    • identification of resources: 접근하려는 자원에 대한 식별
    • manipulation of resources through representations: 표현 방법을 통한 자원 조작
    • self-descriptive messages: 설명이 충분한 응답
    • hypermedia as the engine of application state(HATEOS): 상태 전이를 하이퍼미디어를 통해 제공
  5. Layered System

    서비스를 여러 계층의 레이어로 분리하여 관리한다.

    (미들웨어의 사용으로 Request Parser -> Business Logic -> Response Renderer 처럼 레이어를 나누고 동작을 서로가 이해할 필요 없게 구성하는 방법으로 이해했다.)

  6. Code on demand

    코드를 내려받아서 실행되는 것을 허용하는 것이다. 사전 구현해야 하는 기능의 수를 줄여서 클라이언트를 단순화 시킬 수 있고, 시스템 배포 이후에 확장성을 가지는 이점이 있지만, 이는 가시성을 줄이기 때문에 선택적인 제약 조건이다.

    여기서 말하는 가시성은 프론트 동작을 백엔드에서 주입하므로 흔히들 말하는 스파게티(?)가 될 수 있다는 것 같다.

위에서 6가지 정도의 제약조건을 설명했는데, 글의 서두에서 표현했던 내가 알고 있던 REST API의 요청과 응답 디자인에 대한 내용은 4. Uniform Interface을 확인하면 된다. 사실 나머지는 개발보다는 인프라에 가깝기도 하다.

많이들 알고 있는 2개의 제약 조건

  • identification of resources
  • manipulation of resources through representations

이 두 조건은 글의 처음에서 설명했던 자원에 접근하는 경로와 자원에 할 행동을 말한다. 아래처럼 http 메서드와 경로로 행동과 경로로 다양한 행동을 정의할 수 있다.

  • GET /items/1
  • POST /items

self-descriptive messages

직역하면 스스로 설명이 충분한 메시지인데, 그렇게 어렵지 않은 조건이다. REST API를 만들 때, 응답의 모양 보통 response.data로 접근하는 곳에만 해당하는게 아니라 헤더도 응답에 포함된다는 것을 알고 있어야한다. API를 사용할 때는 헤더라는 것을 통해서 요청과 응답이 어떤 것인지 설명을 한다. "Authorization"으로 인증된 상태로 하는 요청이다라는 것을 말할 수 있고, "User-Agent"로 요청하는 브라우저 혹은 컴퓨터가 어떤 것인지 알릴 수도 있다. 여러 가지 헤더중 "Content-Type"이라는 헤더를 사용하는데, 이 헤더의 용도로 요청과 응답의 데이터를 설명할 수 있다.

GET /item/1
Content-Type: application/json

{
    "name": "new item",
    "content": "new item is created!"
}
# JSON 구조이지만 무슨 내용인진 모름

여기서 사용되는 Content-Type을 통해서 이 응답이 json 형태이며, 응용 프로그램에서 범용으로 사용되는 json 구조다라고 설명해준다.

이 정도면 충분히 설명이 된다고 생각하겠지만, json구조인 것은 알겠지만 아직 안에는 어떤 키와 값을 가지고 있는 지는 설명이 불충분하다.

"Content-Type: application/json" 대신에 "Content-Type: application/item+json"를 사용하고, 어딘가에 "application/item+json"이라는 타입은 key는 name과 content 2가지가 있고, name은 이름, content는 내용이다 라고 정의를 하면 더 많은 설명을 포함할 수 있다. 그러면 item+json 만으로 json형태로 이루어진 item이다라는 것을 content-type만으로 설명이 가능해진다.

GET /item/1
Content-Type: application/item+json

{
    "name": "new item",
    "content": "new item is created!"
}

# JSON 구조이고, name과 content라는 키를 가지고 있군!

이 내용을 보면서 API 문서가 있고, 내용이 충분한데 이런게 필요한가?라는 생각이 들었는데, 잘 만들어진 REST API는 문서가 필요없다고한다...

덧, Content-Type에는 엄청 다양하게 있는데 여기에서 확인하고 새로운 content-type을 등록도 할 수 있다.(등록되면 전세계 개발자들이 확인할 수 있다.)

hypermedia as the engine of application state(HATEOS)

REST는 상태 전이라는 뜻을 포함하고 있다. 상태전이라는 것은 현재 요청의 응답에서 다른 상태로도 전이가 된다는 것인데, 예를 들면 다음 페이지의 목록에 대한 링크를 포함하거나 읽기를 했으니 생성도 할 수 있는 링크를 준다던가 하는 것이다. 백문이 불여일견이니 내가 가장 좋아하는 github api를 보면 좋을 것 같다.

// GET https://api.github.com/users/geusan
{
 "login": "geusan",
 "id": 16855210,
 "node_id": "MDQ6VXNlcjE2ODU1MjEw",
 "avatar_url": "https://avatars.githubusercontent.com/u/16855210?v=4",
 "gravatar_id": "",
 "url": "https://api.github.com/users/geusan",
 "html_url": "https://github.com/geusan",
 "followers_url": "https://api.github.com/users/geusan/followers",
 "following_url": "https://api.github.com/users/geusan/following{/other_user}",
 "gists_url": "https://api.github.com/users/geusan/gists{/gist_id}",
 "starred_url": "https://api.github.com/users/geusan/starred{/owner}{/repo}",
 "subscriptions_url": "https://api.github.com/users/geusan/subscriptions",
 "organizations_url": "https://api.github.com/users/geusan/orgs",
 "repos_url": "https://api.github.com/users/geusan/repos",
 "events_url": "https://api.github.com/users/geusan/events{/privacy}",
 "received_events_url": "https://api.github.com/users/geusan/received_events",
 "type": "User",
 "site_admin": false,
 "name": "geusan",
 "company": "BLUEHORSE",
 "blog": "https://dal.ink/geusan",
 "location": "South Korea",
 "email": null,
 "hireable": true,
 "bio": "나는 오늘도 성장한다.",
 "twitter_username": null,
 "public_repos": 11,
 "public_gists": 1,
 "followers": 35,
 "following": 23,
 "created_at": "2016-01-23T17:58:36Z",
 "updated_at": "2022-05-21T04:31:54Z"
}

내용을 보면 한 user에 대한 데이터를 호출하는데 요상한 url이 엄청 많이 딸려온다. 이 유저에 대한 정보를 확인했으니 관련된 다른 정보를 볼 수 있게 도와준다. 이렇게 상태 전이가 되도록 포함해주는 것이 HATEOS의 내용이다.

진짜 REST API

위의 4개의 조건을 모두 포함하면 아래와 같은 형식의 응답 형태가 나온다.

GET /item/1
Content-Type: application/item+json

{
    "name": "new item",
    "content": "new item is created!",
    "list": "https://myserver.com/items",
    "prev": null,
    "next": "https://myserver.com/items/2"
}

자원 식별이 되는 경로를 가지고, http 메서드로 행동 구분이 되었고, 응답의 모양에 대해 설명이 충분하고, 다른 상태로 전이를 시킬 수도 있다.

대충봐도 알겠지만 복잡하고, 상태 전이를 표현하는 중에 delete나 update 같이 다른 메서드를 사용 해야하는 경우에는 표현이 불충분할 수 있다. 이 경우에는 다음과 같이 하나의 상태전이 표현을 구조화 시킬 수 있을 것 같다.

{
    "update": {
        "url": "https://myserver.com/items/1",
        "method": "PATCH"
    }
}

결론

지금까지 확인한 REST API라는 것은 REST 아키텍쳐를 따르면서 서버를 호출하는 방법이고, REST를 따른 다는 것은 6개의 제약 조건을 충족시켜야한다는 것을 알 수 있었고, 그 중에서 API 개발자의 관점에서는 Uniform Interface라는 조건을 잘알고 충족시켜야한다는 것을 알았다. 내가 알고 있던 것은 REST의 일부만 충족시키는 API였다.

생각보다 REST의 제약 조건을 충족시키기 위해서는 더 많은 작업들을 해야하고 분명 모두가 바로 이해하고 개발에 사용할 수 있는 모양새겠지만 많은 작업으로 개발 속도를 떨어뜨릴 수도 있겠다는 생각이 들었다. REST의 모든 것을 만족하기 보다는 절충할 수 있는 여러 방법들을 찾아보고 팀에 맞는 API 아키텩쳐를 규칙으로 만들면 좋을 것 같다.

json:apiMicrosoft docs(API design)를 확인하면 API 디자인을 어떻게 하면 좋은지 예시를 잘 설명해준다.

Reference