-
[Javascript] 병렬 처리와 함께 작업 순서가 유지되어야 하는 경우언어, 프레임워크/Javascript & Typescript 2022. 4. 6. 20:47
틀린 내용이 있을 수 있습니다.
발견하시면 말씀 부탁드립니다! 🙇
회사에서 운영 업무 자동화에 이용할 API를 개발했다. 해당 API에서는 서드파티 연동을 위해 외부 API 호출을 하면서 동시에 내부 DB 요청도 수행한다. 이러한 일련의 작업을 수행하는데 n초가 소요되며 유저 수(m)만큼 반복해야 하기 때문에 단순히 계산해도 n*m 초 이상이 걸린다. 속도 개선을 위해 병렬로 처리하려 하니 순서가 보장되지 않아 DB 요청에서 데이터가 꼬이는 이슈가 발생했다. (각 작업을 마치고 고유키와 로그를 저장하는 플로우를 위해 순서 유지는 필수이며, 속도 vs 순서 유지 중 하나만 선택해야 한다면 순서 유지를 골라야 하는 상황이다.) 개선 -작업 순서 유지와 함께 병렬 처리- 을 하기 위해, 자바스크립트의 비동기 작업 처리에 대하여 알아보고 테스트 코드를 작성해보기로 했다.
1. async & await 표현식을 통한 비동기 작업의 순차 처리
const promiseFunc = (type, second) => { return new Promise((resolve, reject) => { setTimeout(() => { resolve(console.log(type + ' : delay ' + second + ' millisec'))}, second); }) } // 순차 처리 (순서 유지, 그러나 느림) const strictOrderTasks = async () => { console.log('strict order') await promiseFunc('1', 2000) await promiseFunc('2', 3000) await promiseFunc('3', 1000) await promiseFunc('4', 5000) } strictOrderTasks();
2. map과 for문을 활용해 비동기 작업을 처리했을 때 결과 비교
const seconds = [1000, 2000, 3000, 4000, 5000, 200, 100]; // 순서 유지 안됨 const loopByMap = () => { seconds.map(async sec => { await promiseFunc('map', sec) }) } // 순서 유지됨 const loopByFor = async () => { for (let i = 0; i < seconds.length; i++) { await promiseFunc('for', seconds[i]) } } loopByMap(); loopByFor();
3. Promise.all 메서드에 then chain으로 순서 유지된 작업 리스트 얻기
const secFunc = (ms) => { return ms + 'ms'; } // secFunc(50)실행 결과를 0.05초 후 resolve메서드의 인자로 전달&실행 const veryQuick = new Promise((resolve, reject) => { setTimeout(resolve, 50, secFunc(50)); }); // secFunc(200)실행 결과를 0.2초 후 resolve메서드의 인자로 전달&실행 const quick = new Promise(resolve => { setTimeout(resolve, 200, secFunc(200)); }); // secFunc(5000)실행 결과를 5초 후 resolve메서드의 인자로 전달&실행 const slow = new Promise((resolve, reject) => { setTimeout(resolve, 5000, secFunc(5000)); }); // secFunc(10000)실행 결과를 10초 후 resolve메서드의 인자로 전달&실행 const verySlow = new Promise(resolve => { setTimeout(resolve, 10000, secFunc(10000)); }); console.time('run time') Promise.all([slow, quick, verySlow, veryQuick]).then(res => { console.timeEnd('run time') console.log(res); }).catch(e => console.log(e))
착각하지 말 것: Promise.all이 병렬 처리를 시켜주는 게 아니다. 비동기 함수는 async & await (또는 promise chaining)을 활용하여 처리하지 않는 이상 병렬 처리가 된다.(동시 처리라고 하는 게 더 맞는 건가?) Promise.all은 매개변수로 전달받은 비동기 작업들이 모두 종료될 때까지 기다리며, 모든 작업이 완료되면 결과값으로 이행 순서가 유지된 리스트를 반환한다.
이 내용을 로직에 반영해본다면, 외부 API 호출하는 부분은 병렬 처리하여 속도를 개선하고 각 작업의 리턴 값을 순서대로 받아 DB 요청을 함으로써 고유키 조회 및 로그 저장에서 데이터가 꼬일 일은 없을 것 같다. 요약하면 아래와 같다.
1) 외부 API 호출은 병렬 처리로 변경 => 속도 개선
2) promise.all & then을 통해 결과값이 순서대로 나열된 리스트 반환 받음
3) 리스트를 순회하며 DB 요청 => 순서 유지한 가지 더 생각할 점이 있다. promise.all에 전달되는 비동기 작업 중 하나라도 reject 될 경우 그 외 다른 작업들의 성공 여부와 상관없이 모든 작업이 reject 된다. 아래 테스트 코드에서처럼 p1과 p3은 resolve 되었으나 p2가 reject 되면서 catch로 빠지는 것을 확인할 수 있다.
const p1 = Promise.resolve(1); const p2 = Promise.reject(2); const p3 = Promise.resolve(3); Promise.all([p1, p2, p3]) .then((values) => { console.log(values)}) .catch(e => console.log('e: ', e)); // "e: " 2
몇몇 작업들이 reject 되더라도 이에 영향받지 않고 resolve & reject 결과들이 반환되게끔 하기 위해서는 promise.allsettled 메서드를 사용하면 된다고 하는데 이 부분에 대해서는 다음 포스팅에 정리하도록 하겠다. 참고로 promise.allsettled 메서드는 Node.js 12.9.0 버전부터 사용 가능하다고 한다. (회사 Node.js 버전은 12.9.0보다 낮았던 것 같은데...?🙄)
참고자료
- https://stackoverflow.com/questions/30823653/is-node-js-native-promise-all-processing-in-parallel-or-sequentially
- https://javascript.plainenglish.io/does-promise-all-execute-in-parallel-how-promise-all-works-in-javascript-fffc2e8d455d
- https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
- https://jojoldu.tistory.com/639
'언어, 프레임워크 > Javascript & Typescript' 카테고리의 다른 글
[Javascript] Set에 대한 얄팍한 정리 (0) 2023.05.28 [Javascript] test() 사용시 정규표현식에 g 플래그 포함할 경우 (0) 2023.03.13 [Javascript] 함수형 프로그래밍 (+제너레이터/이터러블/이터레이터) (0) 2021.10.19 [Typescript] 기본적인 타입스크립트 문법과 키워드 정리 (0) 2021.10.06 [Javascript] Scope / Closure / Hoisting (스코프와 클로저 그리고 호이스팅) (0) 2020.07.17