Medium - How NOT to design APIs

원문 - Robert Konarskis

Trend 파악을 위한 Medium 기고문 포스팅 - API 디자인의 나쁜 사례;

Learning from bad examples

저는 친구의 고객사 웹사이트 통합작업을 도와준 적이 있습니다. 해당 웹사이트는 부동산 관리 시스템으로 부동산 매물을 보여주는 것이었습니다. 다행히 부동산 관리 시스템은 API가 있었습니다. 나쁜점은 모든 것이 잘못설계 되어 있었다는 것이죠. 이 기사의 목적은 시스템에 사용할 나쁜 조언을 해주는 것이 아닙니다. 그렇게 개발하지 마시고 API 설계에 대한 올바른 접근을 하는 법을 배우자는 것이죠.

Assignment

제 친구의 고객사는 Bed24라는 시스템을 사용해서 부동산 매물과 다양한 예약 시스템들을 관리했습니다. 그들이 만든 웹사이트의 검색 메커니즘은 날짜를 설정하고 인원수를 선택하면 사용 가능한 숙박정보를 알려주는 방식이었습니다. 평범한 작업처럼 들리시겠지만 문제는 Bed24가 다른 시스템과 통합하기 위한 API를 제공하면서 발생했습니다. 해당 개발자는 설계를 할 때 많은 실수를 했었죠. 이러한 실수들이 어떤 것인지 살펴보면서 어떻게 설계했어야만 했는지 알아봅시다.

Sin 1: Request body format

처음에는 오직 클라이언트의 사용가능한 숙박정보를 가져오는 것이 목적이었으므로 /getAailabilities 호출이 우리의 관심사였습니다. 심지어 이것은 사용가능 여부를 가져오기 위해 호출하는 것이지만 POST 리퀘스트이며 JSON body를 필터처럼 받습니다. 아래의 리스트는 매개변수로 사용가능한 리스트 입니다.

``` { "checkIn": "20151001", "lastNight": "20151002", "checkOut": "20151003", "roomId": "12345", "propId": "1234", "ownerId": "123", "numAdult": "2", "numChild": "0", "offerId": "1", "voucherCode": "", "referer": "", "agent": "", "ignoreAvail": false, "propIds": [ 1235, 1236 ], "roomIds": [ 12347, 12348, 12349 ] } ```

JSON 객체를 뜯어보며 뭐가 잘못되었는지 살펴보겠습니다.

  1. Dates 타입인 checkIn, lastNight, checkOut은 YYYYMMDD 형태로 되어있습니다. 왜 ISO 8601 표준 포맷인 (YYYY-MM-DD)를 쓰지 않은 것일까요? 날짜를 문자열로 바꿀 때 많은 개발자들과 JSON 파싱에서 매우 폭넓게 인정받고 있는 포맷인데 말입니다. 추가적으로 lastNight 필드는 checkOut 필드가 항상 마지막 날의 다음날이므로 의미가 중복되는 필드입니다. 제발 날짜를 인코딩할 때는 표준 형태로 포맷팅하시고 API에 중복 데이터를 넣지 마세요.
  2. numAdult, numChild 필드와 같은 아이디들은 죄다 숫자인데 문자열로 인코딩 되어 있습니다. 왜 이렇게 했는지 모르겠네요.
  3. roomId, roomIds / propId, propIds 같은 필드쌍이 있습니다. 이것은 중복되는 필드일 뿐만 아니라 아이디를 넘겨줄 때 타입 이슈가 발생했습니다. roomId는 문자열인 반면에 roomIds는 숫자를 요구했습니다. 이런 것은 혼란을 유발할 수 있고 파싱할 때도 문제가 될 수 잇으며 백엔드 자체에서도 어떤 때는 문자열로 작업하고 어떤 때는 숫자로하고 했을 겁니다. 같은 데이터를 놓고 말이죠.

제발 표준 포맷팅을 사용하지 않고 중복 필드와 필드 타입에 신경을 써주세요. 이런 부주의한 실수들은 개발자들을 혼란스럽게 합니다. 모든 것을 문자열로 랩핑하지 말아주세요.

Sin 2: Response body format

이전에는 리퀘스트 바디의 포맷을 봤었습니다. 이번에는 리스폰스 바디 포맷을 보면서 어떤 것이 문제인지 살펴봅시다. 우리가 인원수와 날짜 정보를 가지고 사용가능한 숙박정보를 가져오는 것이 우리의 목적이라는 것을 기억해주세요. 아래는 리퀘스트와 일치하는 리스폰스 바디입니다.

Request:

``` { "checkIn": "20190501", "checkOut": "20190503", "ownerId": "25748", "numAdult": "2", "numChild": "0" } ```

Response:

``` { "10328": { "roomId": "10328", "propId": "4478", "roomsavail": "0" }, "13219": { "roomId": "13219", "propId": "5729", "roomsavail": "0" }, "14900": { "roomId": "14900", "propId": "6779", "roomsavail": 1 }, "checkIn": "20190501", "lastNight": "20190502", "checkOut": "20190503", "ownerId": 25748, "numAdult": 2 } ```
  1. onwerId와 numAdult는 갑자기 응답에서 숫자가 되어버렸습니다. 리퀘스트에서는 문자열 이었는데 말이죠.
  2. 숙박정보에 대한 리스트가 없습니다. 대신에 상단에 rommId를 키로하는 객체정보가 있습니다. 이 말은 우리가 사용가능한 숙박정보를 모두 얻기 위해서는 모든 객체를 순회할 필요가 있다는 것입니다. 그러고나서 우리는 roomsavail의 값이 0 이상인지 체크해야 합니다. 위의 코드에서는 심지어 roomsavil이 0 일때는 문자열로 리턴하고 값이 있을 때는 숫자로 리턴합니다. 이것은 자바와 같이 타입을 중요시하는 언어에서는 많은 문제를 일으킬 수 있습니다. 제발 위의 예처럼 이상한 키-밸류 리스트를 보여주지 마시고 적절한 JSON 리스트를 사용해주세요. 그리고 필드의 타입이 바뀌지는 않았는지 꼭 확인해 주세요. 정확하게 포맷된 응답은 아래와 같을 것입니다.
``` { "properties": [ { "id": 4478, "rooms": [ { "id": 12328, "available": false } ] }, { "id": 5729, "rooms": [ { "id": 13219, "available": false } ] }, { "id": 6779, "rooms": [ { "id": 14900, "available": true } ] } ], "checkIn": "2019-05-01", "lastNight": "2019-05-02", "checkOut": "2019-05-03", "ownerId": 25748, "numAdult": 2 } ```

Sin 3: Error handling

이 API의 에러처리는 다음과 같이 구현되었습니다. 모든 리퀘스트들은 200의 응답코드를 리턴합니다. 오류가 발생해도 그렇게 리턴합니다. 이말은 응답의 성공과 실패를 구분할 수 없다는 것이고 응답의 바디를 파싱해서 error값을 조사해야 한다는 것입니다. 그리고 이 API는 6개의 에러코드만 있었습니다.

Error codes of Beds24 API

제발 이와 같이 잘못되었을 때 200을 리턴하지 마세요. 대부분의 개발자와 클라이언트가 인식할 수 있는 HTTP 에러코드를 사용하는 것이 좋습니다. 위의 그림과 같이 1009 에러코드는 401(Unauthorized)로 교체되어야 할 것입니다. 그래야 API 클라이언트들이 업프론트에서 리스폰스 바디를 파싱해야 하는지, 어떻게 파싱해야하는지 알 수 있습니다. 보통의 경우에 에러가 발생했을 때는 400(Bad request), 500(server error) 코드와 함께 적절한 에러 메시지를 리스폰스의 바디에 넣어서 보냅니다.

주어진 API의 에러처리 전략으로 어떤 것을 고르시던 그것이 일반적인 HTTP 표준에 부합하는 지 항상 생각하세요. 이것이 우리의 삶을 더 쉽게 만들어 줍니다.

Sin 4: “Guidelines”

아래는 문서화 목록에 있는 API 사용 가이드 라인입니다.

    1. 호출은 최소한으로 필요한 데이터만 주고 받도록 디자인 되어야 합니다.
    1. 한번에 오직 한번의 호출만 허용되며 처음 호출하면 다음 API 콜은 첫번째 호출이 완료되기 까지 기다려야 합니다.
    1. 다수의 호출은 각 호출마다 몇초의 딜레이를 간격으로 두어야 합니다.
    1. API 콜은 절약하여 사용되어야 하고 비즈니스적인 용도로만 사용되어야 합니다.
    1. 5분의 주기를 초과하여 사용하면 경고 없이 계정이 차단될 수 있습니다.
    1. 우리는 API 함수 사용을 과도하게 하는 모든 접근에 대해 경고없이 차단할 수 있는 완벽한 결정권을 가지고 있습니다.

1,4는 말이되지만 나머지는 다 동의할 수 없군요. 왜 그런지 설명 해드리겠습니다.

2번의 경우에는 REST API를 설계할 때 상태가 없는 것으로 가정되고 특정 시간에 상태가 있으면 안됩니다. 이것은 클라우드 응용프로그램에서 REST API가 유용한 이유이기도 합니다. 상태가 없는 컴포넌트는 뭔가가 잘못되었을 때 자유롭게 재배포될 수 있으며 확장될 수 있습니다. RESTful API를 설계할 때는 정말 상태값이 없는지, 그리고 개발자들이 한번에 하나씩 호출되고 있는지 신경을 쓸 필요가 없게끔 설계를 하세요.

3번은 이것은 모호하고 매우 이상한 가이드라인입니다. 저는 왜 작성자가 이 항목을 가이드 라인에 넣었는지 당췌 이해를 할 수가 없네요, 게다가 몇초 간격의 딜레이라는 것은 부정확한 표현이며 리퀘스트 자체의 외부에서 어떤 프로세싱이 진행되고 있는 것같은 느낌을 줍니다.

5&6번은 무엇이 초과인지 말하지 않았습니다. 초당 10번인가요? 아니면 한번? 또한 특정 웹사이트가 많은 트래픽이 발생한다고 해서 개발자들에게 경고없이 그냥 차단을 시킨다고 하면 이것은 그들이 시스템을 사용하지 않게 만들 것입니다. 제발 정확한 가이드라인을 만들고 규칙을 만들 때도 사용자를 생각해주세요.

Sub 5: Documentation

다음은 API의 문서가 생긴 모양새입니다.

Beds24 API documentation look&feel

여기서 문제점은 가독성과 전체적인 느김입니다. 커스텀 HTML 스타일이 아니라 마크다운 스타일로 작성한게 훨씬 나을뻔 했네요. 아래는 Dilliger를 사용해서 2분안에 더욱 나은 버전을 만든 것입니다.

Styled version of the documentation

제발 API 문서화를 하실 때는 도구를 사용하세요. 간단한 문서는 마크다운 파일로 충분할 수가 있지만 큰 파일과 풍부한 피처가 필요하다면 스웨거나 Apiary같은 것이 사용하는데 더욱 나을 것입니다.

Sin 6: Security

해당 API 문서에서는 함수를 사용하기 위해 API 접근은 메뉴 설정 » 계정 » 계정 접근에서 허용이 되어야 한다고 나와있었죠. 그러나 실제로는 특정 호출을 위한 인증정볼르 보내지 않아도 누구나 리퀘스트를 날리고 데이터를 열어볼 수 있었습니다. 이것은 문서의 내용과 다른 부분이었죠. 문서에서는 JSON 메소드는 계정에 접근하기 위해 API 키가 필요하다고 했고 API 키는 메뉴 설정 » 계정 » 계정 접근에서 설정할 수 있다고 나와있었습니다.

추가적으로 위에서 인증에 대해 잘못된 내용이 있었습니다. API 키를 사용자가 스스로 만들어야 한다는 부분이었죠. 사용자에게 키를 스스로 만들게 하는 것은 잠재적으로 보안이 부실한 키를 만들 수도 있으며 해당 필드에 입력된 값에 대해 포맷팅 문제가 있을 수도 있습니다. 가장 나쁜 것은 SQL 인젝션이나 다른 공격에 뚫릴 수가 있다는 것이죠. 절대로 사용자에게 API 키를 만들도록 하지마세요. 항상 자동 생성되는 변하지 않는 키를 주세요.

해당 리퀘스트들은 실제적으로 인증이 되었지만 다른 문제가 있었습니다. 인증 키가 리퀘스트 바디에 있었다는 것입니다.

Beds24 API authentication example

인증 토큰을 리퀘스트 바디 내부에 있다는 말은 서버가 파싱을 해서 키를 빼낸 다음에 인증을 수행하고 그 다음에 리퀘스트에 따른 작업을 수행한다는 것입니다. 인증이 성공한 경우에는 어차피 파싱할 것이므로 오버헤드가 없겠지만 실패할 경우에는 불필요한 작업을 하게 되는 것입니다. 더욱 좋은 방법은 리퀘스트 헤더에 토큰을 넣어서 Bearer 인증 스키마나 비슷한 다른 것을 쓰는 것입니다. 이 방식에서는 서버가 인증 성공한 경우에만 리퀘스트 바디를 파싱하게 됩니다. 또 다른 이유는 Bearer와 같이 표준 인증 스키마를 쓰는 것이 많은 개발자들에게 친숙하고 사용하기도 편합니다.

Sin 7: Performance

마지막으로 평균적으로 리퀘스트가 완료되는데 1초가 넘게 걸렸습니다. 현대 응용프로그램에서 이런 딜레이는 허용되지 않죠. 그러므로 API 설계 시 성능을 고려해야 합니다.

위의 문제점에도 불구하고 결국 동작하긴 합니다. 그러나 개발자에게 그것을 구현하고 이해하는데 몇배나 더 많은 시간이 걸리게 하고 복잡한 방식으로 코딩을 하게 하죠. 그러므로 API를 배포하기 전에 그것을 사용할 개발자를 먼저 생각해주세요. 문서화를 깔끔하고 잘 구성했는지 확실히 확인해주세요. 자원이름이 규칙을 따르고 데이터가 잘 구조화되었는지, 이해하기는 쉬운지, 사용하기는 쉬운지 확인하세요. 그리고 성능과 보안에 대해서 주의를 기울이세요. 에러 핸들링 또한 잊으시면 안됩니다. 위의 모든 것을 고려해서 API를 설계하셨다면 위의 예제처럼 괴상한 가이드라인 문서를 만들 필요도 없습니다.

Summary

  • API 설계 시 주의할 점
  • 일반적인 JSON 포맷을 사용할 것, 중복된 키-밸류가 없는지 확인할 것
  • 변수의 이름이 컨벤션을 따르는 지, 이해하기 쉬운지 확인할 것
  • 인증 토큰을 헤더에 넣어서 Bearer같은 스키마를 사용할 것, 바디에 넣게 되면 서버에서 인증을 위해 파싱을 해야함, 실패할 경우 오버헤드 발생
  • 퍼포먼스와 보안을 생각해서 설계를 해야 함

© 2019. All rights reserved.

Powered by Hydejack v8.1.1