• TOC

API는 예측 가능하고, 적응 가능하고, 탐색 가능하도록 만들어야 합니다.

예측 가능한 API란?

예측 가능한 API란 일관된 디자인의 API입니다.

일관된 디자인은 사용자의 이전 경험을 활용하여 직관적인 인터페이스를 만드는 데 도움이 됩니다.

일관성이 없는 디자인은 다양성 또는 모순을 불러들여 인터페이스를 이해하고 사용하기 어렵게 만듭니다.

일관된 데이터 디자인이란?

일관된 데이터 디자인이란 리소스, 파라미터, 리스폰스 그리고 프로퍼티의 의미를

컨슈머가 쉽게 이해할 수 있도록 일관적인 이름을 짓는 것으로 시작합니다.

일관적인 이름을 짓는 예시를 살펴봅시다.

  일관성 없는 이름 일관성 있는 이름
예약 목록 조회 reservationId reservationId
예약 정보 조회 id reservationId
예약하기 source sourceReservationId

사용자가 reservationId로 예약 목록을 표현한 것을 봤다면,

디자인 통일성에 익숙한 사용자들은 이런 형태의 정보를 모두 reservationId로 표현될 거라 기대합니다.

따라서, 속성이 전체 예약 목록의 일부건, 예약 목록의 요약이건, 경로 파라미터건, 예약 아이디는 reservationId로 불려야 합니다.

데이터의 구조에 대한 컨벤션이 결정되면 엄격하게 지켜야 합니다.

기본적으로 모든 API의 데이터는 언제나 일관성이 있어야 합니다.

일관적인 목표 디자인하기

API 동작은 목표에 따라 결정됩니다. 당연히 이 목표들도 일관되어야 합니다.

예약 목록 조회, 예약 정보 가져오기보다는 예약 목록 조회, 예약 정보 조회가 일관성이 있습니다. checkReservation()checkReservationInform()으로 코드로 표현할 수 있고, REST API는 HTTP 프로토콜 바탕으로 표현되는 덕분에 프로그래밍적 표현이 일관적으로 구성됩니다. 위 두 가지 목표는 바라보는 경로만 다를 뿐 모두 GET /resource 경로 형태의 표현으로 HTTP 메서드를 사용합니다.

성공이나 실패 피드백을 응답으로 반환할 때도 일관성은 지켜져야 합니다. 모든 목표가 201 Created 나 202 Accepted 같은 유용한 코드들을 반환하지 않고 200 OK만을 반환했다면 새로운 목표는 200 OK 외에 다른 성공 HTTP 상태 코드를 반환하지 않는 것이 더 현명할 수도 있습니다.

일관성은 에러 메시지에도 적용되어야 합니다. 오류를 나타내려면 반드시 일관된 HTTP 상태 코드를 반환해야 합니다. 또한, 반환되는 정보 데이터도 일관성이 있어야 합니다. 필수 속성이 누락되었음을 나타내기 위해MISSING_MANDATORY_PROPERTY와 같은 일반 코드를 정의했다면 API에서 필수 속성이 빠졌을 땐 무조건 이 코드만을 사용해야 합니다.

일관성이란 단순히 API 형태뿐만 아니라 작동 방식에도 영향을 미칩니다.

따라서 인터페이스 계약의 모든 측면과 API의 모든 동작은 일관성이 있어야 합니다.

일관성의 4단계란?

  1. 하나의 API가 일관된 데이터와 목표, 행동 등을 제안함으로써 어떻게 스스로 일관성을 부여하는지 확인
  2. 조직에서 제공하는 API 전체의 일관성, 조직 내의 API 간에도 공통 기능 공유
  3. API가 사용하거나 쓰이는 도메인 내에서 일관성 유지
  4. API가 외부에서도 일관성을 유지할 수 있도록 표준 준수

일반적인 관행과 표준을 준수하기

  • 컨슈머가 의미에 익숙할 가능성이 있으므로 표준을 사용하면 이해가 쉬워집니다.
  • API의 데이터는 표준을 따르는 다른 API에서도 쉽게 수용할 수 있어 API가 가진 상호 운용성도 크게 증가됩니다.
  • REST API는 문서화된 HTTP 프로토콜의 규칙을 준수한다면 일관성을 가질 수 있습니다.
  • API의 내부와 외부에서 일관성을 유지하는 것이 좋습니다.

여러 API 간의 일관성을 유지하려면 다른 API를 디자인할 때도 동일한 규칙을 따라야 합니다. API가 조직 내부의 다른 API와 일관성을 유지하기 위해서는 기존 API 디자인에 부합해야 합니다.

일관성을 유지하는 것은 정말 중요하지만, 맹목적으로 일관성을 적용하는 것도 피해야 합니다. 예를 들어, 일관성을 유지하는 것이 유저 사용성을 저해시키거나 상식에 반대되어서는 안됩니다.

일관성을 유지하는 것은 예측할 수 있게 만든다는 것과 같은 뜻으로 컨슈머가 직관적으로 API를 사용하는 데 도움이 되도록 하는 좋은 방법입니다.

적응 가능한 API란?

같은 개념을 컨슈머에게 맞춰 다르게 표현함으로써 예측 가능하게 만들고, 다른 유형의 컨슈머들까지 고려해서 그들이 원하는 것을 만족시킬 수 있는 API를 말합니다.

왜 적응 가능한 API를 만들어야 하는가?

리소스가 하나 더라도 표현할 수 있는 방법은 다양합니다. 사용자가 사용하려는 형태에 따라서 다르게 응답을 제공할 수 있다면, 사용자는 자신에게 맞는 방법으로 API를 사용할 수 있으므로 더 편리하고 유연한 API가 됩니다.

적응가능한 API는 어떻게 만들 수 있는가?

  1. 다른 포맷으로 제공하거나 응답하기

콘텐츠 네고시에이션(Content Negotiation)을 이용하여 필요에 따라 포맷을 지정하여 요청을 할 수 있습니다. 콘텐츠 네고시에이션은 단일 리소스에 대해서 여러 가지 표현을 통해서도 상호작용을 가능하게 해주는 HTTP 메커니즘입니다.

요청할 때는 Accept 헤더를 이용해서 받고 싶은 미디어 타입을 요청하고, 응답할 때는 Content-type 헤더를 이용해서 어떤 미디어 타입인지 명시해 줘야 합니다.

// 요청
GET /reservations
Accept: application/json

// 응답
200 OK
Content-type: application/json

{
  "reservations": [
    {
			"id": 2,
			"date": "2022-10-10",
			"content": "밥먹기, 코테풀기",
			"status" : "RETROSPECTIVE_COMPLETE" // 회고 작성 완료
		},
		{
			"id": 1,
			"date": "2022-10-09",
			"content": "출근하기",
			"status" : "RETROSPECTIVE_WAITING" // 회고 작성 
		}
  ]
}
// 요청
GET /reservations
Accept: text/csv

// 응답
200 OK
Content-type: text/csv

id,date,content,status
2,"2022-10-10","밥먹기, 코테풀기","RETROSPECTIVE_COMPLETE"
1,"2022-10-09","출근하기","RETROSPECTIVE_WAITING"
  1. 국제화와 현지화

만약 한국어로 반환된 에러 메시지를 프랑스 유저가 받았다면 해당 유저가 메시지를 이해할 수 있을까요? 아마 그렇지 않을 것입니다. 요청 언어에 맞춰 번역된 응답을 제공해 준다면, 다른 언어를 사용하는 유저들도 API 응답을 이해할 수 있을 것입니다. 또한 각 나라마다 다른 규격이나 단위를 사용하고 있다면, 규격에 맞는 응답을 할 수도 있습니다. 이러한 콘텐츠 네고시에이션을 통해 컨슈머가 원하는 데이터뿐만 아니라 딱 맞는 언어와 단위를 제공할 수 있습니다.

// 한국어
POST /transfers
Content-Type: application/json
Accept-Language: ko-KR // 이렇게 사용하는 언어를 헤더에 포함해서 보냅니다.

{
	"source" : "1234567",
	"destination" : "7654321",
	"amount" : 1000,
}

400 Bad Request
Content-Type: application/json
Content-Language: ko-KR

{
	"code":"AMOUNT_OVER_SAFE",
	"message": "금액이 소비 한도를 초과하였음"
}

// 프랑스어
POST /transfers
Content-Type: application/json
Accept-Language: fr-FR

{
	"source" : "1234567",
	"destination" : "7654321",
	"amount" : 1000,
}

400 Bad Request
Content-Type: application/json
Content-Language: fr-FR

{
	"code":"AMOUNT_OVER_SAFE",
	"message": "Le montant depasse le seuil autorise"
}
  1. 필터, 페이지, 정렬 적용하기

만약 예약 내역이 수천 개가 넘는 경우 컨슈머 입장에선 엄청나게 많은 기록을 한 번에 보는 것보다 적당히 나눠서 받기를 원할 것입니다.

예약 내역은 서버에서 가상의 페이지로 나뉩니다. 이때 각 페이지는 pageSize를 지니게 됩니다. 만약 pageSize를 제공하지 않으면 서버는 기본값을 이용해 처리하는데 기본적으로 첫 번째 페이지를 반환합니다. 그중 첫 페이지를 가져오고 싶다면 pageSize=10 으로 주고 page를 1로 설정해서 요청하면 됩니다.

// 쿼리 파라미터 형태
GET /reservations?pageSize=10&page=1

HTTP 헤더의 Range를 이용하는 방법도 있습니다. 첫 페이지로 10개의 예약 내역을 가져오고 싶다면 리퀘스트 헤더의 Range를 items=0-9로 설정해 주면 됩니다.

// http header range option
GET /reservations
Range: items=0-9

탐색 가능한 API란?

API 역시 책처럼 탐색할 수 있게 디자인할 수 있습니다. REST API는 탐색과 관련이 태생적으로 존재하는데, URL과 HTTP 프로토콜을 채택하여 사용하고 있기 때문입니다.

코드숨 공부방 예약하기 API에서 컨슈머는 예약 내역 리스트에 접근할 때, 어떤 페이지를 원하는지 지정해서 요청할 수 있습니다. 그렇다면 여러 페이지를 한 번에 필요로 할 때는 어떻게 할지 알아보겠습니다.

"reservations" : [
   {
      "date": "2022-10-24",
      "content": "공부를 열심히"
		}
]

지금의 컨슈머의 리퀘스트에 대한 서버의 리스폰스는 오직 reservations 속성으로 배열 형태의 예약 내역 객체를 반환할 뿐입니다. 페이지 번호와 목차가 없는 책과 같습니다. 페이지 처리와 관련된 데이터를 추가해 주겠습니다.

{
	// 페이지 처리와 관련된 metadata
 	 "pagination": {
		"page" : 1,
		"totalPages": 9
	},
	"reservations" : [
		{
			"date" : 2022-10-26,
			"content" : "코테 풀기",
			"status": "RESERVED",
			"action" : ["cancel", "modify"]  // 가능한 동작과 관련된 metadata
		}
		{
			"date" : 2022-10-28,
			"content" : "프로젝트 마무리 짓기",
			"status": "CANCELD",
			"action" : []
		}
	]
}

예약 리퀘스트를 처음 할 때 페이지에 관련된 파라미터를 주지 않고 요청한다면, 서버는 현재 페이지 번호(첫 페이지 1) 와 전체 페이지 수 개수(totalPages)를 reservations 배열 속성과 함께 제공하게 될 것입니다. 이 경우 컨슈머에게 8페이지의 예약 목록이 남아있다는 것을 알려주는 것입니다.

이런 추가적인 데이터 덕분에 예약 내역 목록 탐색이 가능해졌습니다. 컴퓨터 공학 분야에서는 이러한 데이터를 메타데이터(metadata)라고 칭합니다. 메타데이터는 데이터를 위한 데이터입니다. 메타데이터는 컨슈머들에게 현재 그들이 어느 페이지에 있으며, 무엇을 할 수 있는지도 알려 줍니다.

메타데이터로 할 수 있는 일은 페이지 처리뿐만이 아닙니다. action이라는 데이터가 새로 추가된 것을 확인할 수 있습니다. 예약 API를 사용하면 컨슈머는 예약을 수정하거나 취소할 수 있습니다. 이미 취소된 예약은 취소할 수 없지만, cancel되지 않은 상태는 취소할 수 있다는 것을 action 데이터로 알 수 있습니다.

API는 메타데이터를 통해서 컨슈머에게 현재 어디에 있는지와 무엇이 가능한지를 알 수 있게 도와줍니다. API에서 이러한 추가 정보들이 필수는 아니지만, 메타데이터는 API의 사용성을 크게 향상 시켜 줍니다.

컨슈머는 예약 목록의 가장 마지막 페이지를 곧장 가져오는 것을 원할 수도 있습니다. 마지막 페이지까지 계속 요청을 보내야 한다면 어떨까요? 심지어 페이지가 천 페이지가 넘는다면? 정말 끔찍할 것입니다. 다행히 하이퍼미디어 API가 이 문제를 해결해 줍니다. 마지막 페이지 번호를 GET /reservations?page={lastPage 값}으로 요청만 하면 되는 것이죠. 코드숨 예약 사이트로 예시를 들어보겠습니다.

{
	// 페이지 처리와 관련된 metadata
 	 "pagination": {
		"page" : 1,
		"totalPages": 9,
		"next": "/reservations?page=2",
		"last": "/reservations?page=9"
	},
	"reservations" : [
		{
			"date" : 2022-10-26,
			"content" : "코테 풀기",
			"status": "RESERVED",
			"action" : ["cancel", "modify"]  // 가능한 동작과 관련된 metadata
		}
		{
			"date" : 2022-10-28,
			"content" : "프로젝트 마무리 짓기",
			"status": "CANCELD",
			"action" : []
		}
	]
}

페이지 메타데이터는 다음 페이지와 마지막 페이지가 무엇인지 속성의 값으로 제공하여 /reservations?page=2는 다음 페이지를, /reservations?page=9 는 마지막 페이지를 호출할 수 있습니다. 그렇게 되면 컨슈머는 사용 가능한 URL이나 URL의 구조를 몰라도 API를 다룰 수 있게 됩니다.

REST API는 컨슈머가 웹 사이트를 탐색하는데 필요한 모든 메타데이터를 제공하는 하이퍼미디어 API와 같습니다. 메타데이터는 단순한 리소스 간의 링크뿐만 아니라 가능한 동작들도 묘사할 수 있습니다. REST 아키텍처 스타일 유니폼 인터페이스는 HATEOAS(hypermedia as the engine of the application state)라고 불립니다.

하이퍼미디어는 href 속성을 각각 가져올 수 있도록 하이퍼링크를 제공해줍니다.

{
	"_links": {
		"self": {
			"href": "/reservation/1234567"
		},
		"transactions": {
			"href": "/reservation/1234567/"
		}
	}
	"id": "1234567"
	"status": "RESERVED"
}

Siren 하이퍼미디어 포맷에서 우리는 cancel 액션을 이용해 예약 취소에 대해서 묘사할 수 있습니다.

{
	"properties" : {
		"date" : 2022-10-26,
		"content" : "코테 풀기"
	},
	"links": [
		{
			"rel": ["self"]
			"href": "/reservation/1234567"
		}
	],
	"actions": [
		{
			"name": "cancel",
		 	"href": "/reservation/1234567",
		 	"method": "DELETE"
		}
	]
}

하이퍼미디어 메타데이터를 제공하는 것이 REST API를 예측 가능하게 만드는 일반적인 방법입니다.

지금까지 우리는 GET, POST, PUT, DELETE, PATCH라는 HTTP 메서드를 사용해 왔습니다. OPTIONS라는 메서드를 이용하면 해당 리소스에서 사용할 수 있는 HTTP 메서드를 가져올 수 있습니다. 이러한 기능들을 선택할 때 컨슈머들에게 정말 도움이 되는지 판단해 보는 것이 우선입니다. 항상 API에서 사용하는 프로토콜의 가능성을 검토해 봐야 하지만, 사용자가 매우 드물게 사용하는 기능까지 고려 대상으로 하는 검토는 피해야 합니다.