프로젝트 진행 시 한번에 약 40개 정도의 API를 호출을 해야하는 상황이 있었다. 생각만해도 많은 문제가 발생할 거 같았는데, 협업의 요구사항이라서 어쩔 수 없이 진행을 하였다. 역시나 많은 문제가 있었지만, 그 중 운영기 반영 시 여러개의 API 호출 시 reponse의 순서가 보장되지 않는 현상 에 대해서 오늘은 서술해보고자 한다.
당시 상황 설명
우측의 화면에 모든 메뉴에 대해 API를 요청하고 response로 테이블 혹은 chart를 그려서 노출했어야 했다.
너무 과도한 API 요청과 수많은 chart의 rendering으로 인해 메모리의 사용량이 상당히 증가하였다.
이러한 UI/UX는 개발자와 사용자 모두에게 좋지않은 영향을 미친다는 것을 현업과 조율하여 페이지를 구분하여 문제를 해결할 수 있었다.
로컬에서 개발 후 개발서버와 운영서버 각각에 반영을 하였을 때 response의 순서 보장되지 않아 원하는 위치에 원하는 reponse 값이 반영되어 있지 않았다.
이 문제는 http의 차이에 있었고 이에 관련해서는 이전 글에서 공부를 하였다.
간단하게 이야기하면 http1.x 의 경우 느리지만 순서를 보장한다는 장점이 있지만 http3.0의 경우 동시에 많은 호출을 할 수 있어서 속도는 빠르지만 순서는 보장되지 않는다는 특징이 있어서 발생한 문제였다.
실제로 사용자가 사용하는 환경은 http3.0 이기 때문에 어떻게든 순서를 보장할 수 있는 로직을 구현해보았다.
현재 VUE.js에서 axios를 이용하여 공통로직으로 만들어 API를 호출하고 있다.
그리고 아래에 코드들은 해당 프로젝트에 맞는 해결책이었지 다른 곳에서 사용하기 위해서는 원리를 이해하고 적용할 필요가 있을 것 같다.
// 1. parameter를 배열에 담아서 전받는다.
let params = [{parameter 1},{parameter 2},{parameter 3},{parameter 4}]
// 2. parameter를 loop를 통해 모두 호출 한다.
params.forEach(param => {
/* api호출을 위한 공통 로직 */
this.$api("/setting/district/getDistrictSummary.do", param, (response) => {
/* response로 로직 구현 */
});
})
위와 같이 코드를 작성했을 때 http1.x에서는 순서가 보장되기 때문에 문제가 없는 것 처럼 보였었다. 하지만 http3.0에서는 큰 문제가 되엇었다.
이러한 점을 보완하고자 두 가지 방법으로 구성을 해보았다.
핵심은 parameter 전달 시 index를 같이 전달하는 것이었다.
index를 같이 전달하고, response 시 같이 전달 받아서 순서(index)를 보장 할 수 있었다.
로직을 간단히 설명하면
const multiParam = params.map((obj, index) => {
return {param: {...obj, index}, url: '/api/stor/compare/storekpi'};
})
/**
* multi API method 실행
*/
multiApi(multiParam, (response) => {
// callback 후 로직은 여기서 작성한다.
});
/**
* multi API method
*
* */
function multiApi(params, fnCallBack) {
const config = {
headers: {
"Content-Type": "application/json",
"auth": Vue.prototype.$getEncrypt()
},
};
axios.defaults.baseURL = '';
axios.defaults.withCredentials = true;
/*
parameter 를 하나 씩 호출하며
promise 를 반환하는 axios를 배열로 담는다.
*/
let responses = params.map((axiosObj) => axios.post(axiosObj.url, axiosObj.param, config))
console.log("axios response", responses)
Promise.all(responses).then((response) => fnCallBack(response))
}
waterfall
을 통해서 볼 수 있다.추가로 reduce를 사용해서 api를 순차적으로 호출하는 로직도 구현해보았다.
reduce를 사용하게 되면 동시에 여러개를 호출하는 것이 아닌, 하나 씩 api를 호출하는 방법이기 때문에 속도 이슈가 발생할 수 있다.
이 방법은 excel export 할 때 용량이 너무 커서 메모리 이슈가 있었는데 이 문제를 해결하기 위해 한번 구현해보았다.
엑셀 다운로드 시 response를 통해 table을 그리고 난 뒤에 export 하는 방식으로 구현했었다. 이 때, 400Mb가 넘는 response를 동시에 여러개 받아서 table을 render 하다보니 메모리 부족 이슈가 발생하였다.
해당 이슈는 table로 그리지 않고 바로 json(response) to Excel 방식으로 수정함으로써 rendering 하는 메모리 누수를 막을 수 있었다.
let iter = urls[Symbol.iterator]();
iterator화를 하게 되면 next()
함수를 통해 (value,done)
을 반환 받는다.
/* 1. url 배열 iterator 화 */
const urls = [
'/api/stor/excel/basic'
, '/api/stor/excel/sales'
, '/api/stor/excel/customer'
, '/api/stor/excel/customer/analy'
, '/api/stor/excel/storekpi'
, '/api/stor/excel/profit'
, '/api/stor/excel/deliverse'
, '/api/stor/excel/marketing'
]
let iter = urls[Symbol.iterator]();
/* value.done이 true가 아니라면 실행 */
const run = (iterator, value) => {
if (!value.done) {
let param = { start_month, end_month }
this.$callApi(value.value, param, (response) => {
/* callback 영역, response 로직 구현 후 재귀 호출
value.done을 채크함으로 무한루프 이슈 없음
*/
run(iterator, iterator.next());
});
}
};
/* 2. 실행 : iterator와 첫번 째 값 전달 */
run(iter, iter.next())
Iterator (반복자)
개발자 모드에서 아래와 같이 테스트를 진해해보면iter.next()
를 할 때 마다value와 done
을 반환받았고, 배열 이상으로 넘어갈 시done = true
가 된다. 이 점을 이용하여 로직을 구현해보았다.
3.2.2에서 언급한 것 처럼 excel 다운로드를 위한 api 호출 시 동시호출(promise.all)과 하나 씩 호출(reduce)의 속도 차이는 얼마나 날지 궁금해서 한번 측정해 보았다.
각 각 세번 씩 테스트를 진행하였고, console.time() 을 통해 소요된 시간을 측정, 그리구 평균을 내었다.
동일한 환경에서 (http1.0) 진행을 하였고 그 결과는 아래 표와 같이 역시 동시에 호출하는게 더 빨랐다. 하지만 그렇게 엄청난 속도 차이를 발견하지는 못했다는 점에서 조금 흥미로운 결과가 아닌가 싶다.
http3.0 에서도 테스트를 해보고 싶었는데, 운영서버에 마음대로 반영하고 테스트를 할 수 없기에 진행하지 못하였다는게 많은 아쉬움으로 남는다.
프로젝트를 진행하면서 겪었던 이슈는 당시에는 엄청 큰 문제로 다가와서 눈물이 날거 같았는데, 이렇게 지나고나면 왜이렇게 작아 보이는지 모르겠다.
당시에 사이트 오픈 하루전 새벽1시에 이슈를 발견해서 진짜 멘탋 붕괴가 제대로 왔었던 기억이 있다. 이 경험으로 http에 대해서 공부할 수 있었고 실무의 변수에 대해서 익힐 수 있어서 뜻깊은 시간이었다.
오랜만에 지난 이슈 기억을 되짚어보니 시간이 지나서 다행이다 싶다.. ㅋㅋㅋㅋ