ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [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();

    순서는 유지되는데 느림. 만약 10초씩 걸리는 작업을 100번 수행해야한다면?

     

     

    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();

    map으로 순환하며 비동기 함수를 실행할 경우 병렬 처리되며 순서 유지 X / for문은 순서 유지는 되나 느림

     

     

    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))

    병렬 처리되어 약 10초가 걸렸으며 작업 순서(완료 순서랑 다름)가 유지된 리스트를 얻을 수 있다

     

    착각하지 말 것: Promise.all이 병렬 처리를 시켜주는 게 아니다. 비동기 함수는 async & await (또는 promise chaining)을 활용하여 처리하지 않는 이상 병렬 처리가 된다.(동시 처리라고 하는 게 더 맞는 건가?) Promise.all은 매개변수로 전달받은 비동기 작업들이 모두 종료될 때까지 기다리며, 모든 작업이 완료되면 결과값으로 이행 순서가 유지된 리스트를 반환한다.

     

    https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/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보다 낮았던 것 같은데...?🙄)

     

     

     


    참고자료

    댓글

jaejade's blog ٩( ᐛ )و