ETC

FE 개발 측면에서 바라본, 쓰기 좋은 Rest API

partner_jun 2023. 3. 5. 22:43

FE 개발자는 많은 Rest API(웹 개발에 대한 이야기이니 이하 API로 칭한다)를 사용한다. 대부분의 로직을 백엔드에서 처리하기 때문이다. 그러다 보니 많은 개발자가 만든 API를 사용해보게 된다. 나 역시도 정부 기관, 오픈 API나 사내 API 등 꽤 많은 수의 API들을 사용해보았다. 겉으로 보기에 큰 차이는 없어 보였지만 유독 쓰기 곤란한 것들도 있었고 오랜 기간 다양한 작업을 수행했음에도 아무런 수정이 필요하지 않았던 훌륭한 API들도 있었다. 물론 비즈니스 로직의 영향도 있고 변경 범위가 크지 않아서 그럴 수 있다. 하지만 API를 호출하는 입장에서 느껴지는 '무엇인가'가 있다. 그 차이가 무엇일까 궁금해 얼마 전부터 떠오르는 것들을 메모장에 적어두었다. 어느정도 내용이 쌓인 것 같아 블로그에 적어두려고 한다. 
 
 

Request

먼저 API를 호출할 때 입력하는 값들에 대해 적는다.
 

Request Header

 
버전 헤더

  • API의 버전을 관리하는 방법으로 URL에 /v1/, /v2/와 같은 방식을 사용하는 것특정 헤더에 버전을 넣어 버전에 맞는 응답을 받는 방식이 있다.
    • 개인적으로 URL에 의한 버전 구분보다 헤더를 이용한 버전 구분 방식을 훨씬 선호한다.
      • 그 이유는 호출하는 입장에서의 버전 관리 때문이다.
      • 버전에 따라 URL 자체를 변경하게 된다면 이전 버전의 API를 폐기했을 때 그에 맞추어 추가적인 작업이 필요해질 수 있다.
      • 버전 헤더를 이용한다면 이전 버전이 폐기된다고 하더라도 헤더의 '최소 값' 처리를 통해 새 버전이 응답되도록 할 수 있다. URL을 통한 관리시에도 가능하지만 이는 문서화 측면에서 악영향이 있다.
    • 버전 구분을 위한 헤더는 X-로 시작되는 커스텀 헤더나 Accept 헤더를 이용한다.
  • 버전은 수정이 있을 때마다 매번 변경하는 것보다 응답의 형태 자체가 변해 호환성에 문제가 생길 때 변경하는 것이 편하다.
    • Spring 기반의 클라이언트가 있는 경우는 제외 (Jackson 사용시 필드 추가적인 어노테이션이 필요하기 때문)

 
디바이스 헤더

  • 사용법 자체는 버전 헤더와 같지만 디바이스 타입에 따라 응답이 변해야 할 때 사용한다.
  • FE에서 직접 디바이스에 따라 응답을 재가공할 수 있지만 그것보다 API에서 변형된 응답을 주는 것이 편하다.
    • 예를 들어 현재 재직중인 회사에서는 Ios/Android 여부에 따라 다른 배너를 응답한다.
  • 다만 웹 사이트 특성상 크롤러를 비롯한 이상한 접근이 많아 기본 값은 필수라고 생각한다.
    • 노출이 다르게 될 경우가 있으므로 기획(정책)적으로 기본 값을 정해둘 필요가 있다.

 
Content-type

  • 기본 값을 반드시 지정해줄 것 
    • 최근 application/json의 형태가 많아 무의식적으로 사용하지만 정부API 등은 아직도 xml을 사용하기도 한다.
    • 이러한 경우 다시 파싱하고 처리하는데 어려움이 있다.
  • x-www-form-urlencoded
    • 주로 POST 메소드를 받는 경우 사용하는데 이 형태는 생각보다 사용하기 어렵다.
    • ECMA 계열에서는 form-data 등 라이브러리를 이용하여 전송할 수 있으므로 크게 신경 쓰이지 않을 수 있지만 Spring 기반 클라이언트에서는 생각보다 해야 할 일이 있는 편이다.
    • 특별한 경우가 아니라면 사용을 지양하는 것이 낫다고 생각한다.

 

Request Body

요청할 때 사용되는 본문 (query parameter, body)에 해당하는 내용이다.
 
 
undefined와 null의 구분

  • 때로 선언되지 않았을 때(undefined)와 null로 전송했을 때 다른 응답을 주는 API를 볼 수 있다.
  • 이해는 되지만 사용하기에 굉장히 곤란해지는 전송 방식이다.
    • 그 이유로는 이 값들이 모두 부정형이라는데 있다.
    • ECMA 사용자 측에서는 이러한 값들을 부정 값으로 판단하게 되는데, 그렇기 때문에 이 값들은 모두 비슷하게(심지어는 같게) 느껴진다.
      • 심지어 로직상, if (value) 와 같은 형태를 사용할 수 없게 된다.
    • 따라서 1 / 0 / undefiend / '' 와 같은 형태는 사용자에게 큰 혼란을 준다.
  • 결과적으로 요청에서 undefined를 구분하는 것보다 다른 플래그를 추가하는 것이 더 낫다.

 
요청받는 필드는 '특별한' 형태를 위해 사용

  • 우리는 흔히 특별한 행동을 하지 않는 경우를 일반적이라고 생각한다.
  • 또한, ECMA 개발 측에서는 논리적으로 값이 없는 경우를 부정으로 생각한다
    • 예를 들어 { "isBlocked" : isBlocked } 로 요청하는 경우, block되지 않은 값을 얻고 싶다면 isBlocked null / undefined / false 어떠한 값이든 들어갈 수 있다는 것이다.
      • undefined라는 것은 isBlocked를 명시하지 않아도 요청이 가능하다는 뜻인데, 
        다시말해 특별한 작업을 하지 않는다면 필터링되지 않은 값을 응답한다는 뜻이다.
        • 우리는 흔히 block되지 않은 상태를 일반적이라고 생각하기 때문에 이 파라미터는 이해하기 쉬워진다.

 
필수 값, 타입의 명확한 정보

  • API를 문서화한다고 해도 유지보수 측면에서 명확해야 한다.
    • 값이 정말 논리적으로 필수 값인지에 대해 고민이 필요하다.
    • 대부분의 필드는 기본 값으로 설정이 가능하며, 기본 값이 많을수록 테스트 및 유지보수가 편해진다.
  • 또, 타입 캐스팅이 활발하게 일어나는 ECMA를 위해 기왕이면 다양한 타입을 받아주거나, 일치하지 않는 타입이 왔을 때 명확하게 에러를 응답해주는 것이 좋다.

 
 


 

Response

API를 통한 요청의 응답 값에 대해 적는다. 아마 대부분은 JSON 형태로만 사용하지만 특별한 경우 쿠키를 사용하거나 의도적으로 Cache-Header를 이용해 캐싱하기도 한다.
 

Response Header

 
Error Status: 500 Internal Server Error

  • 흔히 API는 문제가 생겼을 때 500 에러를 반환한다. 하지만 500 에러를 반환할만한 오류가 맞을까?
    • 사전적으로 500 에러는 '서버가 예상하지 못한 상황'에 놓였다는 것이다.
      말 그대로 예외를 핸들링하지 못한 경우 발생하는 것이 맞다.
    • 예를 들어 요청 값에서 필수 값인 'id'를 누락했다고 했다면 이는 500 Internal Server Error가 아니라,
      400 Bad Request가 응답되어야 한다.
      • 심지어는 id에 일치하는 값이 없으므로 null이 응답되어도 된다.
  • 500 응답을 남용하게 되면 생기는 문제가 두가지 있다.
    • AWS의 ELB 등에서 서버의 상태가 좋지 못하다고 판단할 수 있다.
      • 최악의 경우 시스템은 LB에서 서버를 제외시키고 오토스케일링으로 새로운 서버를 붙여버릴 수도 있다.
      • 이는 곧 허접한 크롤러 하나가 쓸데없는 동작을 계속해서 반복하게 만들 수 있다는 뜻이다.
    • 클라이언트에서 추가적인 처리가 필요하다.
      • Typescript에서 Promise.allSettled를 사용한다고 할 때 Reject 발생시 타입 처리가 곤란해진다.
      • 대부분의 에러가 500으로 응답된다고 하면, 에러 내용에 대한 팝업을 노출해야 할 때 에러 응답의 body를 살펴보아야 한다.
        • 문제는 서버에서 500을 응답한게 아니라, ELB나 CF 등 다른 인프라 시스템에서 에러를 응답한 경우다.
        • 이러한 경우는 body가 없을 수 있으니, 예외 상황에 의해 발생한 에러 노출의 예외를 처리해야 한다.
  • Sentry 등 모니터링 툴에서 식별 가능한 플래그 삽입
    • 클라이언트 사이드에서 500 에러 발생시 잡히는 로그가 단순하면 문제를 해결할 수 없는 경우가 많다.
    • 장기적으로 API를 관리하고 추적하려면 플래그를 삽입할 필요가 있다.

 
CORS

  • Access-Control-Allow-Origin 헤더와 관련된 내용은 아직도? 가 아니라 지금도 신경써야 하는 문제이다.
  • 요즘 추세로 보면 직접 요청하는 것보다 아무래도 리버스 프록시를 사용하는 경향이 크긴 하다.

 
쿠키

  • API 및 AJAX 콜도 쿠키를 설정할 수 있다. 이는 곧 세션도 사용할 수 있다는 뜻이다.
    • 서비스 기업에서 흔히 사용하는 Rest API라면 사용하지 않겠지만 크롤러 개발 등에서는 중요한 부분이 될 수 있다.
  • 개발에는 늘 특별한 상황이 있을 수 있다는 것을 인지해야 한다.

 

Response Body

 
부정/긍정 필드

  • 앞서 이야기한 내용이지만, 부정/긍정에 따라 ECMA에서 처리하는 방식이 달라진다.
  • API의 응답에서는 필드명이 긍정인 경우가 편하다.
    • Request Body의 필드와 반대로, 특별한 경우인 경우에만 마킹하는 것이 일반적이기 때문이다.
    • 특히 JS에서는 undefined, null이 부정이라는 점을 다시 한번 상기할 필요가 있다.
    • 응답으로 { "isError" : true } 가 온다면 이는 에러가 발생했다는 뜻이다.
      • 반대로 { }. isError가 undefined라면, undefined는 부정 값이므로 응답은 에러가 발생하지 않았다는 것을 알 수 있다.

 
응답의 형태는 대체로 객체가 좋다.

  • 특정 키를 찾아야 하는 로직이 있다면 배열보다는 객체 형태로 반환하는 것이 좋다.
    배열 사용시 키를 찾기 위해 루프를 돌아야 하기 때문이다.
    • 물론 비즈니스 로직 상 특정 키가 없을 수 있는 응답의 경우에는 객체를 사용하는 것이 좋다.
  • 심지어 ECMA에서는 객체 형태의 응답도 무리없이 이터레이션 할 수 있다.
    • Object.keys, Object.values 같은 유용한 유틸리티성 함수들이 있기 때문이다.
    • 물론 Spring 기반 클라이언트가 있을 수 있다면 객체보다는 배열이 낫다.

 
다국어 지원(i18n) 처리를 위해 응답 코드 혹은 문장 노출을 고려해야 한다.

  • 최근 사용되는 프레임워크들의 i18n은 번역을 위해 key-value 형태로 선언된 일종의 번역 사전 파일을 사용한다.
  • 서버로부터 받은 응답을 그대로 번역 사전에 등록할 수 있다면 작업이 한결 쉬워진다.
  • 또, 사람이 식별할 수 있는 응답일수록 유지보수에 용이해진다.

 
API의 기능별 분리

  • 여러 데이터들을 하나의 API로 묶어 전송할지, 기능별로 API를 나누어 전송할지에 대해서는 시간을 두고 생각해 볼 필요가 있다.
  • 대체로 백엔드의 API 유지보수에 있어서는 분리하는 편이 낫고, 프론트엔드의 작업 측면에서는 하나로 묶인 것이 낫다.
    • 브라우저에서 한번에 다운로드 받을 수 있는 수가 제한되어 있어 많은 API를 클라이언트에서 직접 호출하는 경우 FE 성능 하락으로 이어진다.
    • 특정 응답이 느릴 수 있다는 점, 스크롤 위치에 따른 Lazy rendering을 고려해야 한다거나 같은 API를 그대로 이용해 앱 개발을 해야 하는 경우 등 케이스에 따라 충분한 고민이 필요하다.
  • API 요청 수가 늘어남에 따라 TPS가 하락하여, API 서버를 스케일아웃 해야 할 수 있다는 점도 고려해야 한다.
    • 당연하게도 API의 응답 용량이 커질수록 더 심하게 성능이 떨어지게 된다.

 
타임존 (DateType)

  • 날짜가 포함된 데이터는 타임존을 신경쓰지 않고 개발하면 서비스 확장 및 유지보수에 큰 걸림돌이 될 수 있다.
    • 한동안 epoch time을 즐겨 사용했지만 최근 몇가지 작업을 하다 보니 ISO 문자열이 더 낫겠다는 생각이 들었다.
  • ISO 문자열의 가치
    • 주기적으로 다시 되새기지만 계속해서 잊는 점인데, API의 응답은 사람이 알아볼 수 있어야 편하다.
    • 시간 정보가 없는 파싱(Date.parse 계열)은 엔진마다, 브라우저마다 차이가 있을 수 있다는 것에 주의해야 한다.
      • 나 역시도 이 문제로 고통을 받았었는데, 타임존과 얽혀 문제를 찾기 위해 너무 오랜 시간을 소비해야 했다.
      • moment.js와 같은 라이브러리를 사용하면 조금 더 나을 수 있지만 용량이 크고 대부분의 기능을 사용하지 않다보니 아무래도 적용하는 것이 꺼려진다.

 
불필요한 항목은 항상 제외할 것

  • 이 내용과 정확히 매칭되는 것은 echo성 응답이다.
    요청한 값을 그대로 돌려주는 형태로 개발되기도 하는데, 이는 불필요한 정보일 가능성이 높다.
  • 응답의 용량이 크면 서버의 TPS가 하락하여 성능상 손해를 보게 된다.
  • 특히, 이 값을 추가하게 되면 요청할 때 쓴 값을 사용해야 할지,
    서버에서 재처리된(정말 재처리 되었는지도 알 수 없다) 값을 사용해야 하는지에 대해 FE 개발자는 고민하게 된다.
  • 재미있는 것은 FE 개발자가 고민해서 선택하더라도 다른 개발자에게 인수인계 하면 또 다시 같은 고민을 한다는 것이다.

 

 
사실 FE 개발자 입장에서 선택의 여지가 거의 없는 것이 사실이다. 하지만 이전 글에서 이야기한 것과 같이 FE 개발자라고 하더라도 결국 풀스택 개발을 하게 되는 추세로 보인다. 결국 어느정도의 API 개발을 하게 될 텐데, 더 나은 개발을 위해 API를 사용하며 불편했던 점들을 기억하고 본인의 양식으로 삼을 필요가 있다. 지금 사용하는 것이 이상하다면 기억해두고 반면교사 삼자. 또, 기회가 된다면 BE 개발자에게도 이야기 해 줄 필요도 있다. 대부분의 개발자들은 향상심을 가지고 있으니 경험을 쌓아 점점 더 나아지지 않을까.