데이터와 목표와 성격에 맞는 커뮤니케이션 적용 방법

API의 목표와 데이터의 특성에 따라 단일하고 동기적인 리퀘스트/리스폰스 기반의 메커니즘은 효율적인 표현이 아닐 수도 있습니다. 발생할 수 있는 각 상황에 맞는 데이터의 목표와 성격에 맞는 커뮤니케이션을 적용해야 합니다.

처리 기간이 오래 걸리는 작업 관리하기

목표의 특성에 따라 동기화된 리퀘스트/리스폰스 메커니즘이 불가능할 수도 있습니다. 컨슈머가 일정 시간 동안 리스폰스를 받기 위해 대기해야 한다는 것은 처리까지 오랜 시간이 걸리는 것을 의미합니다. 이런 경우 리퀘스트의 처리 상태를 나중에 받도록 하고, 언제 다시 후속 리퀘스트를 보내면 되는가에 대한 정보는 프로토콜에서 제공하는 기능을 선택해 사용하거나 단순히 관련 데이터를 반환함으로써 컨슈머와 프로바이더 사이의 불필요한 호출을 피할 수 있습니다.

예를 들어 코드숨 공부방을 이용하려면 계획을 필수로 작성해야 하는데, 계획을 잘 작성했는지는 사람이 확인해야 합니다. 따라서 사용자가 예약 요청을 보내면 즉시 ‘예약 완료’를 응답하지만 실제로는 예약은 보류 중이며, 관리자가 계획이 잘 작성됐음을 확인한 후에 실제로 예약이 수행됩니다.

컨슈머에게 이벤트 알리기

컨슈머에서 프로바이더까지의 통신은 항상 효율적인 방식으로 이뤄지진 않습니다. 때로는 프로바이더가 주도권을 갖는 편이 더 유용하게 돌아갈 수도 있습니다.

컨슈머가 반복적으로 API를 호출하는 것을 폴링이라고 합니다. 불필요한 호출이 빈번하게 발생되어 컨슈머나 프로바이더 양쪽 모두 성가신 방법입니다. 코드숨 예약 조회를 계속해서 한다면 불필요한 호출이 계속되는데, 컨슈머는 예약이 되었는지 확인할 수 없어 불편할 것입니다. 실제로 예약이 완료되었을 때 이를 컨슈머들에게 알려준다면 매우 좋을 것입니다.

컨슈머 / 프로바이더의 통신 방향을 역으로 뒤집는 것은 웹훅을 통해 할 수 있으며, 이는 종종 “리버스 API” 라고 불립니다.

사용자는 사용자의 예약에 대해 검증을 요구한다면 코드숨 예약 API는 202 Acceptd 상태 코드로 리퀘스트가 수용되었으며 나중에 곧 처리될 것임을 응답합니다. 이제 사용자의 웹 애플리케이션은 규칙적으로 송금 상태를 묻는 폴링을 할 필요가 없어졌습니다. 예약이 실제로 완료되면 코드숨 공부방 예약 API은 POST 리퀘스트로 코드숨 예약하기 웹훅 URL에 이벤트를 발송할 것입니다. 리퀘스트의 바디에는 발생한 이벤트에 대한 정보가 들어있으며, 구체적으로는 예약 ID, 이벤트의 상태 같은 것들이 포함되어 있을 겁니다.

코드숨 공부방 예약 웹의 백엔드 구현에서 웹훅 이벤트를 수신하면, 사용자에 해당하는 식별자를 찾고, 이메일 SendGrid 이메일을 통해 알림을 전송해 예약이 완료되었음을 안내할 수 있습니다. 이러한 메커니즘은 컨슈머가 유발하는 비동기 통신으로만 제한되지는 않습니다. 컨슈머와 아무런 상호작용 없이도 이벤트가 생성되어 컨슈머에게 알려야 하는 때도 있습니다.

웹훅 API를 디자인할 때도 프로바이더의 관점을 지양해야 하며, 사용 가능하고 발전 가능하게 디자인해야 합니다. 필요한 것이 무엇인지에 따라서 하나의 웹훅이 가능한 모든 이벤트를 수용하거나, 여러 개의 웹훅이 각 이벤트별로 하나씩 존재하게 될 수도 있습니다. 웹훅 API가 단순해서 구현하고, 소비하기 편하다면, 새로운 이벤트를 추가하는 것도 쉬울 겁니다. 이전 디자인적 선택에 앞서 항상 여러분들이 속한 컨텍스트를 고려하기 바랍니다.

이벤트 흐름 스트리밍 하기

API가 항상 변경되는 데이터를 기본 리퀘스트 / 리스폰스 목표를 사용하여 컨슈머에게 제공하는 경우에는 컨슈머들이 지속적으로 폴링을 시도하리란 걸 명심해야 합니다. 이러한 반복적인 API 호출은 그저 새롭거나 수정된 데이터를 가져오기 위해서일 뿐입니다.

SSE(Server Sent Events)에서 스트림을 처리하는 상황을 이러한 경우 맞춰 보여주고 있습니다.

환율 API를 통해서 특정 달러 기준 가격에 대한 상세 정보를 제공해 줍니다.

// HTTP SSE를 이용해 컨슈머에게 이벤트 스트리밍하기
GET /api/live?accsess_key=e30cs8cc73c2b3b7bb3d9617csdg2c

200 OK
Content-Type: application/json
{
		"success": true,
    "terms": "https://currencylayer.com/terms",
    "privacy": "https://currencylayer.com/privacy",
    "timestamp": 1545881647,
    "source": "USD",
    "quotes": {
			"USDKRW": 1121.419945,
			"USDVND": 24842.5
		}
}

컨슈머는 GET /api/live 리퀘스트에 적절한 Accept 헤더에 토큰을 넣어 환율 API에 대해서 application/json 문서 형태로 제공해달라고 요청했습니다. 매분 환율 정보를 반환합니다.

SSE를 통한 문제 해결은 가격 정보가 스트림 형태로 제공된다는 점입니다. 반환된 데이터는 정적이지 않으며 종결된 상태는 아닙니다.

SSE를 이용하면 서버는 컨슈머에게 이벤트 데이터를 보낼 수 있습니다.

하지만 SSE에 대해서 알아야 할 사항이 몇 가지 있습니다.

  • SSE는 HTTP 프로토콜에 의존하지만 포함되는 사항은 아닙니다. 이 표준은 W3C에서 HTML5 표준으로 만들었습니다.
  • 브라우저 기반 컨슈머를 위해 디자인된 표준으로 그들이 사용하기에는 매우 편리합니다. 그렇지만 그 외의 경우에도 대부분의 언어로 만들어진 라이브러리가 존재합니다.
  • 이벤트 데이터는 오직 텍스트만 가능합니다. 만약 여러분들이 이미지와 같은 바이너리 데이터를 보내고 싶다면, 이들을 텍스트 인코딩해야 합니다.
  • SSE 스트림은 HTTP 압축의 이점을 활용할 수 있습니다. 이는 단방향 스트림으로 연결이 활성화되면, 컨슈머는 이 연결을 통해서는 데이터를 서버에게 보낼 수 없음을 의미합니다.

이벤트에는 가능한 한 적은 양의 데이터를 넣고, 자세한 정보가 필요하다면 컨슈머가 정규 API를 호출하는 편이 좋습니다. 그렇지만 스트리밍 유즈케이스에서는 사용하는 기술에 따라서는 더 많은 데이터를 제공해 주는 편이 더 좋습니다.

SSE는 HTTP 프로토콜에 의존하기 때문에 별도의 인프라 구성이 필요하지 않습니다. 그렇지만 SSE를 사용하면 기본적으로 HTTP 연결이 오랜 시간 동안 열려있을 수 있으므로 API를 호스팅 하는 인프라는 이러한 긴 병렬연결을 유지하고 성능 이슈가 없도록 무조건 튜닝해야 합니다.

이벤트를 스트리밍하는 방법에는 여러 가지가 있습니다. 중요한 점은 API 디자이너가 리퀘스트/리스폰스 메커니즘만이 유일한 해법이라고 생각해서는 안 된다는 점입니다. 유동성이 매우 큰 데이터나 실시간 데이터를 취급할 때는 이벤트를 스트리밍하는 것이 프로바이더에서 컨슈머뿐만 아니라 컨슈머에서 프로바이더에게 이벤트를 스트리밍하는 때도 고려 해봐야 합니다.

여러 요소 처리하기

API는 단일 리소스만 처리해야 한다는 법은 없습니다. 경우에 따라서는 한 번의 호출로 여러 리소스를 처리하는 것이 유용한 컨텍스트가 있습니다.

만약 토요일과 일요일 예약을 미리 한꺼번에 하고 싶은 경우, POST /reservations 호출을 여러 번 보낼 수도 있고, 여러 개의 예약을 한꺼번에 body에 담아 한번의 호출로 해결할 수도 있습니다. 여러 건의 예약을 확인 처리하기 위해서 컨슈머는 id와 reserved 속성을 확인된 예약 별로 제공해 줘야 합니다.

이렇게 여러 상태를 반환할 때 207 Multi-Status 상태 코드를 사용하면 여러 리소스를 한 번의 호출로 관리하기 쉬워집니다.

207 Multi-Status
{
	"reservations": [
		{
			"status": "200 OK",
			"body": "{\"id\":\"T123\"}"
		},
	  // ...
		{
			"status": "400 Bad Request",
			"body" : "{\"message\": \"Invalid...\"}"
		}
	]
}

API를 디자인할 때 고려해야할 컨텍스트는 무엇인가?

효율적이고 사용 가능하며 구현 가능한 API를 제공하기 위해 목표 또는 데이터의 본질에 주의를 기울여야 합니다.

이 모든 것들이 의미하는 바는 API를 디자인하는 것은 단순히 컨슈머에 집중하고 프로바이더 관점을 피하기만 하면 되는 게 아니라는 점입니다. API가 가능한 모든 컨슈머들의 요구를 수용할 수 있도록 (그리고 프로바이더에서 구현할 수 있도록) 하기 위해서는 소비 및 제공되는 컨텍스트를 모두 온전히 이해하고 있어야 합니다.

컨슈머의 기존 관행과 제약사항 숙지하기

모든 컨슈머의 요구를 충족한다는 것은 필요한 모든 목표를 이해하기 쉽고 사용하기 쉬운 방식으로 제공하는 API를 디자인하는 것을 의미합니다. 이는 비기능적 요구사항이라는 관점에 주의해야 합니다. 비기능적 요구사항이란 기본적으로는 API 목표와 데이터가 어떻게 표현될 것인지에 대한 이야기입니다.

컨슈머들은 API를 디자인할 때 고려해야 할 특정 관행이나 몇 가지 제약사항이 있을 수 있습니다. 관행을 따르면 이해하기 쉽고, 사용하기 쉬우며, 상호 운용성이 뛰어난 API를 디자인할 수 있습니다.

적절한 표현을 선택하는 것은 API 디자이너에게 익숙한 것, 마음에 드는 디자인, 유행하는 것 따위를 선택하는 것이 아닙니다. 컨텍스트를 따라 원하는 내용에 부합하는 표현을 선택해야 합니다.

프로바이더의 한계를 신중하게 고려하기

api 디자인을 할 때 순수하게 내부적인 고려 사항을 컨슈머에게 노출하는 것을 피하라고 했지만 프로바이더의 관점을 노출시키지 않는 것을 모든 상황에서 지키라는 것은 아닙니다. 예를 들어 증권 거래소는 항상 개장된 상태가 아닙니다. 컨슈머가 증권 거래소가 개장하지 않은 상태일 때 주식 매수를 하고자 하면 처리가 불가능하다는 에러가 나올 겁니다. 이 경우 컨슈머들에게 주식 시장의 개장시간을 알려주면 좋을 것입니다.

주식거래 시 주어진 금액 이상의 국제송금은 사람에 의해 검증되어야만 합니다. 따라서 기본적인 동기방식의 request/reponse 방식 목표로는 표현될 수가 없어 비동기 표현을 택해야만 합니다.

프로바이더의 제약은 단순히 기능적인 영역뿐 아니라 기술적인 영역에서도 발생할 수 있습니다. 기술적 제약사항은 일반적으로 응답시간을 중심으로 시스템의 확장성, 가용성, 네트워크의 제약이 있습니다. cpu를 추가하거나, 구현을 최적화하거나, api의 디자인을 조정해 문제를 해결할 수 있습니다. 주의해야 할 사항은 api를 디자인할 때 reqeuset 전후로 실제 무슨 일이 발생하는지 깊이 알고 있어야 기능적, 기술적 제약사항을 포착할 수 있습니다. 프로바이더 영역에서의 기능적, 기술적 제약사항들은 다양한 형태만큼 커뮤니케이션을 적용시키거나 특정 목표를 위한 목표 별 도구성, 입출력 프로퍼티들 또는 에러 처리와 같이 다양한 해법들을 갖고 있습니다.

컨슈머와 프로바이더에 적절한 API 스타일은 어떻게 선택해야 하는가?

원격 API를 디자인하는 데 사용할 도구를 선택할 때는 익숙한 것, 유행하는 것 또는 개인적 선호에 따라 결정해선 안됩니다. 오직 컨텍스트에 따라 결정되어야 하며, 올바른 도구를 선택하려면 적어도 둘 이상은 알아야만 합니다.

각각의 도구들은 유용한 쓰임새가 있는데, 처한 상황, 즉 컨텍스트마다 다릅니다.

REST API는 로이 필딩이 선보였던 REST 아키텍처 스타일을 준수하는 API를 말합니다.

gRPC는 구글에 의해서 만들어진 프레임워크로 RPC는 Remote Procedure Call을 의미합니다. RPC API는 단순하게 함수를 외부에 노출하는 방식입니다.

GrpahQL API는 데이터 스키마에 접근하여 컨슈머가 정확히 필요한 데이터를 가져갈 수 있게 제공합니다.

어떠한 API 택했건 간에, 사용자, 목표, 입력, 출력, 그리고 에러를 식별해 내야 하며, 또한 프로바이더 관점이 아닌 가능한 최대한 컨슈머 지향적인 표현을 제공해야만 합니다.

여러분들의 컨슈머가 누구인지, 그들의 상황을 이해하고 난 뒤에, 그들이 필요로 하는 목표들은 무엇인지, 그것들을 어떻게 쓸 것인지, 그리고 프로바이더의 상황을 이해하고 난 뒤에야 적합한 API를 선택할 수 있습니다. 이렇게 사례별로 컨텍스트는 다르겠지만, 요즘에는 REST를 기본으로 택하는 것이 주류입니다. 만약 매우 특수한 요구사항이 있거나 REST API로 대응할 수 없다면 GraphQL이나 gRPC를 사용할 수 있습니다.