본문 바로가기

소프트웨어-이야기/Web

Zalando 사례를 통해 REST API 가이드 살펴보기

zalando는 패션, 신발, 뷰티 부문을 판매하는 독일의 온라인 커머스 기업이다. zalando에서 작성한 Restful API 가이드의 몇가지 사례를 통해 좋은 API를 설계하는 방법을 정리해보고자 한다. 

https://opensource.zalando.com/restful-api-guidelines

설계 원칙

API가 첫번째이다. 

  • 기능을 구현하기 전에 API를 미리 설계해야한다.
  • 클라이언트와 동료들에게 사전에 API 리뷰를 받아야한다. 

추가로 API를 설계할 때 고려해야하는 점은 다음과 같다.

  • 도메인과 기능의 목적을 제대로 이해해야한다.
  • 일반화된 리소스를 제공해야한다. 
    • 특수한 사례별로 API를 제공하는 것은 피해야한다. 
  • API는 무엇을 제공하는지만 표현해야한다.
    • 어떻게 기능을 제공하는지는 API에 포함되면 안된다.
    • 기능의 구현 방법은 추상화해야한다.

이렇게 정의한 API는 Swagger처럼 표준화된 API 관리 도구로 제공하는 것이 좋다. 

  • API 스펙을 한 곳에서 중앙관리 (SSOT: single source of truth)할 수 있다.
  • API 검색, GUI, 자동 품질 검사 등을 인프라 도구로 관리할 수 있다.

설계 원칙 사례

구분자로 리소스와 하위 리소스를 구분해야한다.

API에 하위 리소스를 표현해야하는 경우, 하위 리소스가 상위 리소스에 포함되도록 설계해야한다. 

/resources/{resource-id}/sub-resources/{sub-resource-id}
/shopping-carts/de:1681e6b88ec1/items/1

 

복합키를 식별자 리소스로 표현하라.

복합키가 리소스를 가장 잘 표현하는 식별자인 경우, 슬래시를 사용하여 리소스 정보를 표현할 수 있다.

/resources/{compound-key-1}[delim-1]...[delim-n-1]{compound-key-n}
 
예시는 다음과 같다.
/shopping-carts/{country}/{session-id}
/shopping-carts/{country}/{session-id}/items/{item-id}
/api-specifications/{docker-image-id}/apis/{path}/{file-name}
/api-specifications/{repository-name}/{artifact-name}:{tag}
/article-size-advices/{sku}/{sales-channel}
그러나 복합키를 리소스 식별자로 공개한 경우, 다른 리소스 식별자를 추가하기 어려워진다는 점을 유의해야한다.
 

Date/Time 속성은 _at 접미사로 끝난다.

boolean과 구분하기 위해, date/time 속성에는 _at 접미사를 붙인다. 

created_at << created
modified_at << modified
occurred_at << occurred
returned_at << returned
 

boolean 타입을 null으로 반환하면 안된다. 

boolean 타입인 값이 null을 반환하는 경우가 있다. boolean은 true 혹은 false만 반환해야한다. 

특정한 의미를 지닌 null 을 반환하는 경우, 별도로 정의된 상태 값을 반환해야한다.

예를 들어, accepted_terms_and_conditions라는 속성이 있는 경우, YES, NO, UNDEFINED. 세가지 값을 가질 수 있다. 

 

비어있는 배열은 []으로 내려준다.

비어있는 배열을 null으로 반환하면 안된다. 반드시 빈배열로 내려야한다.

 

Delete API에서 쿼리 파라미터를 사용할 수 있다.

DELETE /resources?param1=value1&param2=value2...&paramN=valueN
삭제 요청에서 쿼리 파라미터가 사용될 수도 있다. 여기서 쿼리 파라미터는 리소스를 선택하기 위함이 아니라 필터링 목적으로 사용된다. 

 

Gzip 압축을 권장한다.

너무 많은 요청을 처리하느라, Gzip 압축 시간이 병목 현상을 일으키는게 아니라면 

요청 응답을 Gzip 으로 압축하는 것을 권장한다.

그래야 네트워크를 통해 데이터를 빠르게 전달할 수 있고, 프론트가 더 빠르게 응답할 수 있다. 

 

응답 속성을 필터링하여, 응답 크기를 줄일 수 있다.

(1) 필터링을 하지 않은 케이스

GET http://api.example.org/users/123 HTTP/1.1

HTTP/1.1 200 OK
Content-Type: application/json

{
  "id": "cddd5e44-dae0-11e5-8c01-63ed66ab2da5",
  "name": "John Doe",
  "address": "1600 Pennsylvania Avenue Northwest, Washington, DC, United States",
  "birthday": "1984-09-13",
  "friends": [ {
    "id": "1fb43648-dae1-11e5-aa01-1fbc3abb1cd0",
    "name": "Jane Doe",
    "address": "1600 Pennsylvania Avenue Northwest, Washington, DC, United States",
    "birthday": "1988-04-07"
  } ]
}

(2) filter 파라미터로 필요한 속성만 추려낸 케이스

GET http://api.example.org/users/123?fields=(name,friends(name)) HTTP/1.1

HTTP/1.1 200 OK
Content-Type: application/json

{
  "name": "John Doe",
  "friends": [ {
    "name": "Jane Doe"
  } ]
}

 

Offset Pagination 보다는 Cursor Pagination를 사용하라.

cursor pagination은 offset pagination보다 더 효율적이고 좋다. 특히, 크기가 큰 데이터베이스를 사용하는 경우, cursor pagination을 사용하는게 좋다. 

pagination을 선택할 때에는 몇가지 트레이드 오프가 있기 때문에 다음을 고려해야한다. 

1. offset pagination가 더 대중적인 기법이다. 그래서 이를 지원하는 프레임워크, 클라이언트 라이브러리 등이 더 많다.

2. cursor pagination은 특정 페이지로 점프하는 기능을 제공하지 않는다. 예를 들어, cursor pagination으로는 페이지 1에서 5으로 넘어갈 수 없다.

3. offset pagination은 중간에 데이터가 추가되거나, 삭제되면 데이터가 중복으로 조회되거나 누락될 수 있다.

4. offset pagination은 서버에서 효율적으로 처리하기 어렵다.

특히 데이터베이스 메인 메모리에서 다룰 수 없는 정도의 데이터 크기이거나, 샤드로 관리되거나 NoSQL으로 구성된 DB인 경우, 적용하기 어렵다.

5. cursor pagination은 전체 데이터 수를 알아야할 때 사용하기 어렵다. 

ref. https://opensource.zalando.com/restful-api-guidelines/#cursor-based-pagination

 

Pagination을 사용할 때에는 Pagination 응답 객체를 사용하라.

{
  "self": "http://my-service.zalandoapis.com/resources?cursor=",
  "first": "http://my-service.zalandoapis.com/resources?cursor=",
  "prev": "http://my-service.zalandoapis.com/resources?cursor=",
  "next": "http://my-service.zalandoapis.com/resources?cursor=",
  "last": "http://my-service.zalandoapis.com/resources?cursor=",
  "query": {
    "query-param-<1>": ...,
    "query-param-<n>": ...
  },
  "items": [...]
}
클라이언트에서는 pagination을 자체적으로 구성하면 안된다. 서버에서 pagination에 필요한 정보를 내려줘야한다. 
pagination 메타 정보에 대한 고려사항은 다음과 같다.
  • query에는 필터링 되어야하는 쿼리 파라미터가 담긴다.
  • pagination link 를 제공해야 한다.
    • link는 단순한 이름으로 지어야한다.
    • ex. next, prev, first, last, self
  • 명확한 요구사항이 있는 경우가 아니면 전체 카운트를 내려주는건 지양해야한다. 
    • 전체 카운트를 조회하는건 성능과 시스템에 문제를 일으킬 수 있다.
    • 복잡한 쿼리가 사용되는 API인 경우, 테이블 풀스캔을 할 수도 있다. 

 

API 버전 관리는 최대한 사용하지 말아라. 

api path에 version을 포함시켜 관리하는 경우, 시스템의 복잡성을 높이고 유지보수를 어렵게 만든다.

때문에 api는 기존 api와 호환되도록 구현해야한다.

만약 기존 api와 호환시킬 수 없는 경우, 다음과 같은 대안을 사용할 수 있다.

1. 리소스를 새로 판다.

2. 엔드포인트를 새로 딴다.

3. 버전이 적용된 신규 api를 기존 버전과 호환되도록 구현한다. 

그러나 api 버저닝을 지양하기 때문에, 최대한 1안 혹은 2안을 사용할 것을 권장한다. 

 

추가로 읽어보면 좋은 REST API 자료