포켓서베이 서비스는 설문 조사를 제작하는 것부터 시작해, 카카오톡 챗봇을 이용한 참여, 마지막으로 인공지능 보고서를 제공하는 일련의 과정들을 모두 제공한다. 기존에는 서비스 이용자들이 주로 만족도 설문조사를 위하여 이용하였지만, 시간이 지남에 따라 카카오톡 알림톡 심사 조건이 완화되었다. 덕분에 조사를 진행할 수 있는 분야의 확장성이 확보되었고, 다양한 형태로 설문을 제작 및 운용할 수 있었다.
학술 조사, 사내 설문조사, 시장 조사 등의 복잡도가 높은 설문이 생겨나면서, 자연스럽게 인공지능 보고서가 분석해야 하는 문항의 개수나 데이터의 양이 급격히 늘어났다. 프론트엔드는 분석된 데이터를 기반으로 차트를 나타내주어야했기 때문에 데이터를 얼마나 잘 가져오고, 빠르게 보여줄 지를 고민해야 했다.
서비스 초기에는 해당 설문 전체에 대한 데이터를 단 한 번의 호출로 불러와 보고서를 보여주고 있었다. 문항 수도 많지 않았고, 데이터 양 자체가 적었기 때문에 데이터 호출에 걸리는 시간이나 렌더링 시간에 크게 신경을 쓰지 않아도 될 정도였다. 하지만 설문의 복잡도와 사용자수가 높아짐에 따라 다양한 형태의 설문들이 출현했고, 데이터의 호출을 설문 단위가 아닌 문항별 단위로 쪼개야 했다.
문항별로 API 호출을 쪼개니, 통신 횟수는 증가했지만 통신당 트래픽 절감으로 각 API의 응답 시간이 훨씬 빨라졌다. 하지만, 문항별로 값을 요청하는 순간에 분석에 대한 데이터가 생성되어 진행되는 것은 마찬가지였다. 결과적으로 문항이 엄청 많은 설문은 여전히 인공지능 보고서 조회를 할 때 오랜 시간이 걸렸다. 최다 건수로 진행한 설문을 기준으로 하였을 때, 최대 10초 이상의 시간이 걸려야 보고서 조회가 가능한 경우도 있었다.
최근에는, 공무원 연금공단에서 총 29만 건의 설문을 진행하는 일도 있었다. 수 만 건에 대한 분석 정보를 조회 요청과 동시에 실행하게 된다면...아마 보고서를 조회하던 사람은 미처 데이터를 돌려받기도 전에 포기하고 돌아갈 거다.
예상대로 사전에 작성이 끝난 보고서 데이터를 가져오기만 하는 건 수 초 이내에 가능했다. 경험적으로 아무리 보고서 내용이 많아도 10초 정도면 불러올 수 있음을 알고 있었기에, 보고서가 미리 작성되어 있는지를 판단하는 기준을 10초로 설정했다. 만약 인공지능이 모든 문항별 데이터 호출에 대하여 10초 이내에 전부 대답하지 못한다면 보고서 작성이 미리 안 된 설문으로 간주하면 되는 것이다. 재빨리 조회 요청이 들어온 보고서를 작성하게 하고, 보고서 작성이 덜 되었다는 사실에 대해서는 서비스 이용자가 설문 분석중이라는 사실을 알 수 있도록 안내를 해주기로 하였다.
안내를 할 지 말 지에 대하여 판단할 수 있도록 하는 것이 주어진 과제였다.
10초라는 시간 제한에 대해서 암시를 걸고 작업을 시작했다.
"나는 지금부터 10초 짜리 에러 시한폭탄을 제조해야 한다."
지금부터는 폭탄 제조를 위해서 시도한 여러가지 방법들을 순차적으로 기술해보려 한다.
서버로 데이터를 요청하기 위하여 사용하는 axios 인스턴스에는 timeout이라는 구성값이 있다.
config 설정 시에 timeout이라는 이름으로 정의하면 적용이 가능하다. 단위는 흔히 사용하는 javascript의 setTimeout이나 setInterval과 같은 ms 단위이다.
timeout 값이 존재하는 axios 호출의 경우, 해당 시간을 초과하도록 응답이 없으면 에러를 발생시켜 catch 구문으로 진입하게 된다.
const axios = require("axios") axios.post("http://sample.url", {name: "peter"}, {timeout: 1000}) .then(response => console.log(response.data)) .catch(err => console.log(err))
처음 구상한 폭탄 처리 방법은 아래와 같다.
이 방법에서는 각 문항별 API 호출을 구성하는 axios 인스턴스의 timeout 속성값으로 평균 호출 시간을 ms 단위로 지정만 해주면 된다.
//questions //callTime for(let i = 0; i < questions; i++) { axios.post("http://sample.url", {name: 'peter'}, {timeout: callTime}) .then(response => console.log('제시간에 도착')) .catch(err => console.log('느려터진 굼벵이')) }
그런데 이 때에 발생하는 문제는, 문항 전체로 보았을 경우에는 10초가 넘지 않는데, 특정 문항 하나의 데이터가 상대적으로 많아서 평균 호출 시간을 넘을 수도 있다는 것이다. 정확한 10초 재기가 불가능해지는 것이다.
"axios 인스턴스로 통제하려는 것이 잘못된건가?" 라는 생각이 들었다.
문항별 API를 구성하는 axios 인스턴스들을 모두 기다리기 위하여 Promise.all 안에 인스턴스 배열을 적용하여 호출하고 있었다.
Promise.all은 어떤 배열 내부에 존재하는 프로세스들이 모두 끝나기를 기다리는 아주 참을성 많은 녀석이다. 그런 착한 녀석을 이용해먹기로 작정하고 작전을 짜 보았다.
정상적인 axios 인스턴스들 사이에 10초 후에 터지는 에러 Promise를 추가하는 것이다.
//에러 폭탄 let fatMan = new Promise((resolve, reject) => setTimeout(function(){resolve(new Error)},10000)) .then(err => console.log('펑!'))
문항이 3개인 설문의 경우를 가정해보면 다음과 같은 형태일 것이다.
let timeout = Promise.all([axios1, axios2, axios3, fatMan])
폭탄이 제대로 터지는지 확인하기 위해서, 시간을 줄여놓고 테스트를 해가면서 값을 확인했다.
불을 붙여놓고 잘 터지기를 기도했지만...폭탄은 불발이었다. 아니, 터지긴 했지만 효과가 없었다.
timeout의 형태가 아래와 같이 나왔다.
Promise.all이 워낙 참을성이 넘치는 녀석이라서, error를 토해내는 녀석이 있더라도 resolved로 넘어가고 마는 것이다... 결국 이 방법으로는 10초라는 시간을 기다리게 만들 수는 없었다.
혹시 fatMan이 에러 자체를 resolve하고 있어서 넘어가는 건가 싶어 폭탄을 다른 형태로도 만들어 보았다.
//에러 폭탄2 let littleBoy = new Promise((resolve, reject) => setTimeout(function(){reject(new Error)},10000)).catch(err => err)
이만큼 신경을 써서 폭탄을 제조해주니 효과가 있었다. 제대로 터진 것이다.
그런데 문제는, 시한폭탄으로 만들어지지 않고 바로 터져버리는 것이다...
Promise.all 내부에서 반환한 setTimeout의 타이머가 원하는 시간 후에 resolve를 시키는 것이 아니라, 곧바로 resolve를 토해내고 있었다.
아무래도 Promise.all 같이 성격 좋은 녀석을 터트리는 건 힘들어 보여서, 비슷하면서도 다른 녀석으로 눈을 돌렸다.
Promise.race라는 녀석은 이름처럼 race를 좋아하는 녀석이다. 속해있는 모든 녀석들을 경쟁시킨 뒤에 1등만 예뻐해주는 지독한 실력지상주의자다.
const promise1 = new Promise((resolve, reject) => { setTimeout(resolve, 500, 'one'); }); const promise2 = new Promise((resolve, reject) => { setTimeout(resolve, 100, 'two'); }); Promise.race([promise1, promise2]).then((value) => { console.log(value);} // promise1과 2 모두 resolve 되지만, 2가 100ms만에 resolve 되므로 더 빠르다 //value는 'two'로 나타나게 된다
처음에는 이전에 시도했던 방식과 동일하게 axios 인스턴스 묶음 사이에 폭탄을 심어놓았다. 이 때 미처 생각하지 못한 점은, 이 녀석이 1등만 기억하는 녀석이라는 것이다. 폭탄이야 터지든 말든, 먼저 도착한 녀석만 다루기 때문에 수많은 API 호출 중에서 먼저 응답한 데이터 하나만 다룰 수 있게 되어버리는 것이다...
이 시점에서 Promise.all이나 Promise.race 둘 중 하나만으로는 원하는 방식으로 시간을 다룰 수가 없다는 것을 깨닫고 한동안은 고민에 잠겼다.
그러던 중 떠오른 생각.
그렇게 Promise.race 내부에 Promise.all을 적용해보게 되었다. 모든 axios 인스턴스들을 담은 배열을 Promise.all로 불러오는 작업 자체를 첫번째 주자로 놓고, 10초 시한폭탄을 두번째 주자로 두는 방법이다.
Promise.race([ Promise.all(axiosInstances), new Promise(resolve => { setTimeout(() => resolve(new Error('펑!!!')), 10000) }) ])
마침내 이 방법으로 원하는 제한 시간을 설정하는 것이 가능하였다!
결과적으로 Promise.race를 실행한 후의 결과가 error일 경우에만 예외 처리를 수행하는 쪽으로 마무리를 해주었다.
해당 작업을 추가한 덕분에 서비스 이용자는 불필요하게 수 십 초를 기다리며 인공지능 보고서가 나타나기를 기도할 필요가 없어졌다.
미처 보고서 작성이 되어있지 않은 설문이라고 하더라도, 조회를 시도하면서 요청이 들어갔기 때문에,
분석이 진행중이라는 안내를 확인하고 잠시 다른 일을 하고 돌아오면 된다. 잠시 후 확인하려던 설문의 보고서를 다시 조회하면 바로 확인이 가능할 것이다.
결과적으로 서비스 이용자 입장에서는 보고서를 보기 위해서 낭비하는 시간을 줄일 수가 있어서 좋고,
서비스 제공자 입장에서는 이용자의 만족도가 높아지는 일이니,
Win - Win이 아닐 수 없다.
단순히 서비스를 만들고 제공하는 입장이 아닌, 사용자의 입장에서 서비스에 대해 생각할 수 있게 된 경험이었다.
QR 스마트 업로드 (feat.카카오톡 챗봇) (2) | 2020.07.23 |
---|---|
웹 설문 영상 미리보기 (video src에 사용되는 영상의 코덱) (0) | 2020.07.21 |
Javascript 엔진 (0) | 2019.08.31 |
190824 오늘의 Keyword (0) | 2019.08.24 |
190823 오늘의 Keyword (0) | 2019.08.23 |
댓글 영역