압축, 상태 유지, 캐싱, HEAD 리퀘스트 등
작은 서비스를 만들 때는 서버(프로바이더)와 클라이언트(컨슈머) 간의 통신에 대해 큰 고민을 하지 않고, API를 개발할 수 있습니다. 그러나 서비스가 커지고 컨슈머 측에서 요청하는 데이터가 많아질수록, 테이블 간의 조인이 많이 발생하고, 쿼리량이 늘어날수록 전체 시스템에서 연산하는 속도는 느려지게 됩니다. 사용자들에게 편의성을 주려고 추가한 기능들이 결과적으로 느리고, 불만족스러운 서비스가 됩니다. 이번 글에서는 간단한 방법으로 서비스 성능을 크게 향상할 수 있는 API 개발 팁을 다룹니다.
리스폰스 데이터를 압축하는 방법입니다. 압축을 하게 되면 속도를 잃는 것과 얻는 곳이 존재하는데, 프로바이더 측에서 데이터 압축에 시간이 소요됩니다. 그러나 압축을 하게 됐을 때 들어가는 시간과 컴퓨팅 자원보다 전송량이 감소됨으로 얻는 속도 이점, 그리고 데이터 전송 용량의 감소로 인한 운영비용의 감소는 결과적으로 더 고성능의 서버를 사용할 수 있게 합니다. 물론 컨슈머 쪽에서 압축된 데이터를 원래 상태로 복원하는 과정은 필수적이겠으나 대부분의 대형 서비스에서 압축은 필수적입니다.
압축을 필수적으로 사용해야 하는 또 다른 시나리오는 네트워크 환경이 안 좋을 때입니다. 대한민국은 LTE가 널리 사용되고, 경우에 따라서 5G까지도 지원하기 때문에 네트워크 속도로 인해서 불편을 겪는 일이 적은 편입니다. 그러나 대부분의 국가 및 사용자의 디바이스 종류나 성능에 따라 네트워크, 컨슈머의 처리 속도 상에서 딜레이가 생길 여지가 큽니다. 동일한 1MB의 데이터를 전달했을 때에 5G 환경에서는 사용자 경험에 크게 영향을 주지 않는 수준의 시간 차이를 보이지만, 초당 1MB 전송이 이뤄지는 곳에선 1초의 딜레이가 생기게 됩니다. 이를 압축해 전달한다면 압축률에 따라서 유의미한 속도 향상이 발생하게 됩니다.
더 좋은 소식은 전송량이 줄어든다는 것은 온프레미스 환경에서 더 적은 시간 동안 네트워크 연결이 개방됨을 의미합니다. 그러므로 대역폭에서 문제가 발생할 가능성을 낮추고, 결과적으로 비용도 낮추게 됩니다.
사용자의 많은 정보를 항상 최신화된 데이터로 유지해야 하는 경우가 있을 수 있습니다. 대표적으로 은행, 결제와 관련된 앱에서 처리되는 데이터는 보통 수십 개의 API를 통해 데이터가 나눠서 들어오게 되고, 이 데이터들을 프로바이더는 암호화하여 보안할 뿐 아니라 은행사에서 다루는 어마어마한 사이즈의 RDS에서 조인된 결과물을 조회할 것입니다. 그러나 사용자가 매번 들어올 때마다 이 데이터를 최신화하여 조회하고, 매 요청마다 수십 개의 API를 요청한다면 아무리 방대한 API 서버와 정교한 데이터베이스를 갖추고 있어도 속도 저하나 비용 문제를 감당하기 어렵습니다. 이를 방지하기 위해서 사용자의 상태를 서버와 유지하는 방법을 사용할 수 있습니다.
이를테면 은행 사용자의 정보 중 대부분의 정보는 불변하는 정보에 해당합니다. 고객의 성명, 계좌번호 등과 같은 데이터를 비롯해 고객과 관련된 수많은 메타데이터들 고객이 수정을 할 수 없거나, 거의 수정을 하지 않는 데이터일 가능성이 높습니다. 그렇기 때문에 고객이 로그인한 시간부터 15분 ~ 30분 정도에 재요청에 대응하여 컨슈머와 프로바이더 모두가 고객의 상태를 유지하고, 중복 API 호출을 방지하는 방식으로 설계를 합니다. 수십 개의 API 요청 중 변경되는 아주 소수의 데이터만 요청에 따라 응답하고, 프로바이더의 컴퓨팅 자원 일부를 고객의 재요청에 대응해 상태 유지를 해주는 방식입니다.
이러한 방식은 여러분들이 사용하시는 대부분의 모바일 결제 앱, 은행 앱, 심지어 가상화폐 거래소 앱에서도 찾아볼 수 있고, 앱뿐만 아니라 웹에서도 동일한 메커니즘으로 동작하는 걸 보실 수 있습니다.
상태 유지가 프로바이더와 컨슈머 측 모두에게서 일어나는 일이라면 캐싱도 유사한 형태라 볼 수 있습니다. API 요청에는 캐싱 시간을 설정할 수 있습니다. 특정 시간만큼 데이터를 저장해 재요청을 방지하여 더 빠르게 동작할 수 있게 만듭니다. 웹에서는 브라우저 레벨에서 이미지, 영상들의 큰 용량이 저장되는 경우에 자동적으로 캐싱 처리가 되고, 그 밖에도 API 자체적으로도 캐싱 시간을 설정할 수 있고, 또한 API를 캐싱하여 반복 요청에 대응할 수도 있습니다. API 캐싱의 쉬운 방법은 레디스를 사용하는 것인데, 이에 대한 내용은 아래의 글에서 확인하실 수 있습니다.
https://brunch.co.kr/@skykamja24/575
만약 캐싱에 대해서 깊은 설명을 보기를 원하신다면 RFC 7234에서 캐싱 부분에 대해 읽어보시는 것을 추천드립니다.
많은 백엔드 개발자 분들이 HEAD에 해당하는 HTTP Method를 모르거나 왜 사용하는지, 어떻게 사용하는지 모릅니다. HEAD 리퀘스트는 GET 요청과 유사하게 서버의 리소스에 대해 조회하지만 콘텐츠를 반환하지 않고, 헤더만 반환하는 메서드입니다. API 향상 방법과 연관 지어 생각해본다면 모든 조회 요청을 GET을 사용해 만들게 되면, 캐싱에 따라 서버에서는 304 Not Modified 리스폰스를 보내줄 수 있지만 이 과정도 생략하여 오직 헤더만 받고자 한다면 HEAD 리퀘스트를 보내 사용자의 데이터가 변화가 있었는지만 체크할 수도 있습니다.
범용적인 API 개발을 하다 보면 컨슈머가 요청하는 데이터보다 더 많은 데이터를 보내주는 오버 패칭(Overfetching)이 쉽게 나타납니다. 오버 패칭을 줄이기 위해 컨슈머의 각 요청에 따라 맞춤으로 API를 개발하는 게 힘들다 보니 적당히 큰 데이터를 보내주는 셈입니다. 서비스 사이즈가 작은 상황에서 오버 패칭은 큰 이슈가 되지 않지만, 오버 패칭은 결과적으로 해결해야 하는 기술 부채라 할 수 있습니다.
페이스북은 오버 패칭을 줄이기 위해, 그리고 온갖 형태의 요청에 대응하기 위해 GraphQL을 만들어, 컨슈머가 원하는 데이터를 정의해서 보낼 수 있도록 했습니다. 물론 GraphQL이 아직 RESTful API를 밀어내고 있다라 말하기 어렵지만, 오버 패칭을 방지해야 리턴 페이로드를 줄일 수 있습니다. 이는 단순히 성능적인 측면뿐만 아니라 보안 측면에서도 유리한데, 사용자 레벨에서 조회하면 안 되는 수준의 데이터가 프로바이더의 오버 패칭으로 쉽게 노출되는 경우가 종종 발생합니다. 보안의 측면에서나 성능의 측면에서나, 비용의 측면에서나 오버 패칭은 줄여야 하고, 될 수 있으면 각 리퀘스트에 대해 필요한 데이터에만 리턴할 수 있게 만들어야 합니다.
앞서 설명한 오버 패칭에서 유사한 내용이지만 필터링된 데이터를 보내는 것도 성능 향상에 직결됩니다. 리스트 형태의 데이터를 불러올 때 개발자는 findAll() 형태로 모든 데이터를 조회하도록 만들고, 컨슈머 레벨에서 필터링을 해서 데이터를 보여주는 방식을 쓸 수도 있습니다. 데이터 사이즈가 크지 않다면 문제 될 것이 없지만 조금만 사이즈가 커지고, 복잡도가 생기는 경우라면 이러한 방식은 최악의 개발 방법이 됩니다.
요청한 데이터에 대해서만 잘라서 가져오고, 심지어 그 정보도 화면에서 파싱 되어야 하는 부분의 데이터만 가져오는 게 좋습니다. 만약 테이블 형태의 데이터라면 오버 패칭을 줄여, 각 칼럼에 표기되는 값에 대해서만 조회를 합니다. 즉 완전한 필터링이 적용된 데이터만 요청하고, 반환합니다. 이러한 필터링, 그리고 페이지 네이션 처리 과정에서 필수적으로 사용되는 필터 항목과 커서 포인터(조회된 데이터의 가장 마지막 인덱스)만 가지고, 성능 향상에 크게 기여할 수 있습니다.
보통 API를 개발할 때 특정한 기능이나 테이블을 기준으로 데이터를 처리하곤 합니다. 사용자라면 User, 계좌라면 Account와 같은 형태로 네이밍을 하고, 해당 데이터에 해당한 정보만을 가져옵니다. 그러나 컨슈머에서 요청하는 데이터가 여러 테이블에 걸쳐서 조합된 데이터를 조회하여 이를 호출하기 위해 여러 API를 호출해야 한다면 이를 하나의 데이터 집합체로 만들어 제공하는 방법으로 성능 향상을 이끌어 낼 수 있습니다.
데이터 집합체를 만들 때 프로바이더가 이를 구현할 수 있는 방법이 생각보다 많습니다. N개의 테이블의 조인이나 연산이 들어간 정보를 가져와야 하는 경우라면, 단순히 라우트 네이밍을 분리해주는 방식으로 컨슈머의 API 호출 숫자를 줄일 수 있습니다. 그러나 프로바이더 입장에서 해당 라우트는 여러 로직을 처리해야 하는 복잡한 구조가 될 것입니다. 이를 조금 더 개선해본다면 집합체 만들 위한 테이블을 별도로 만들어 일정 주기로 fetching 하여 연산된 결과를 일시적으로 테이블에 저장해 둘 수 있습니다. 즉 해당 테이블은 Viewer 용도로 사용되는 테이블이며, 여러 데이터의 집합체로만 존재하는 것입니다.
마지막으로 개선할 수 있는 방법은 API를 계층화하여, 다른 레벨의 API를 제공하는 것입니다. 이를테면 일반 사용자가 사용하는 API 레이어와 관리자가 사용하는 API 레이어를 분리해 제작할 수 있습니다. 일반 사용자와 관리자는 접근하는 데이터 레벨이 다르고, 캐싱되어야 하는 데이터 수준도 다르기 때문에 API 레이어를 독립적으로 가져가는 것은 매우 현명한 방법입니다. 또한 일반 사용자 사이에서도 API의 레이어를 만들어 관리할 수 있습니다.
가령 기존의 기능들이 api.myservice.com에서 지원되고 있다면, 신규 기능들에 대해서는 api-v2.myservice.com이라는 서브도메인을 분리하여 신규 기능들만 관리하고 개발하는 것입니다. 이러한 엔드포인트 분리 방식도 계층화의 한 가지 방법이 될 수 있고, 또는 동일 엔드포인트를 가지는 경우라면 사용자별로 사용하는 기능이나 API 액세스 스코프 수준에서 계층을 정의하는 방식으로도 앞선 여러 아이디어를 실행할 수 있습니다.
웹 개발, 블록체인 컨트렉트 개발 문의: