ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JS] 비동기 작업들의 순차실행과 병렬실행
    Javascript, Typescript 2021. 11. 9. 22:55
    반응형

     

    어떤 결과를 만들기 위해 5개의 비동기 작업을 수행해야 한다고 가정해보자.

    5개의 작업이 서로 연관되어 있어서 작업을 한 번에 하나씩 끝내야 한다면, 작업을 순차적(sequential)으로 처리해야한다. 하지만 5개의 작업이 서로 연관이 없어서 동시에 5개의 작업을 끝내도 상관이 없다면, 작업을 병렬적(parallel)으로 처리할 수 있다.

     

    순차실행(좌), 병렬실행(우)

     

    1초의 수행시간이 걸리는 비동기 작업을 다음과 같이 만들어보았다. 이 함수는 실행하고 1초 뒤에 task finished!라는 로그를 출력할 것이다.

    function handleTask(id) {
      return new Promise(resolve => {
        setTimeout(() => {
          console.log(`task${id} finished!`);
          resolve();
        }, 1000);
      })
    }

    이 handleTask 함수를 통해서 자바스크립트에서 비동기 작업들을 순차적으로 실행하는 법과 병렬적으로 실행하는 법을 알아보자.

    순차 실행

    우리에게 실행해야 할 5개의 task들이 배열에 담겨 있다. 5개의 태스크에 대해 순차적으로 handleTask 함수를 실행해야 한다.

    forEach ❌

    우리는 자바스크립트를 사용하면서 배열의 메소드를 이용한 함수형 프로그래밍에 익숙해져 있다. 그래서 가장 먼저 떠올리는 코드가 다음과 같을 것이다.

    function main() {
      const tasks = [1, 2, 3, 4, 5];
      
      console.log('before');
      
      tasks.forEach(async task => {
        await handleTask(task);
      });
      
      console.log('after');
    }
    before
    after
    task1 finished
    task2 finished
    task3 finished
    task4 finished
    task5 finished

    하지만 main함수를 실행하면, 실행은 1초 만에 끝나버리고 task1~5 finished! 로그 5개가 동시에 찍힌다. 우리의 main함수는 task들이 모두 끝나기 전에 종료될뿐더러, task들을 순차적으로 실행시켜주지도 않는다.

    위 코드에서 forEach 메소드는 5개의 새로운 Promise를 만들고 그 Promise는 그 안에 있는 하나의 handleTask 함수의 이행(resolve)만을 기다린다. 각각의 Promise는 서로에게 아무런 영향도 주지 않는다.

    for...of

    배열 메소드를 이용한 함수형 프로그래밍을 포기하고 자바스크립트를 사용하면서 쓸 일이 없을 거 같던 for...of 문을 이용해 작성해본다.

    async function main() {
      const tasks = [1, 2, 3, 4, 5];
    
      for (const task of tasks) {
        await handleTask(task);
      }
    }

    1초에 하나씩 task n finished! 로그가 찍히다가 5초가 지난 후에 main함수가 종료되면서 실행이 종료된다.

    우리가 원하던 비동기 task들의 순차 실행이다. 사실 이 방법이 가장 간단하게 비동기 함수의 순차 실행을 구현하는 방법이기 때문에 이렇게 구현한다고 나쁜 코드는 절대 아니다.

    그렇다면 배열메소드를 이용해서 함수형으로 해결하는 방법은 없을까? 있다. reduce 메소드를 이용한 프라미스 체이닝(promise chaining)을 이용해서 비동기 함수의 순차 실행을 구현할 수 있다.

    reduce

    async function main() {
      const tasks = [1, 2, 3, 4, 5];
    
      await tasks.reduce((prevTask, currTask) => {
        return prevTask.then(() => handleTask(currTask));
      }, Promise.resolve());
    }

    reduce 메소드의 초깃값으로 이행된 Promise를 넘겨주고(Promise.resolve()), tasks 배열을 돌며 이전 Promise의 이행을 기다렸다가 현재 task를 실행하는 새로운 Promise를 반환한다. 그리고 main 함수는 reduce 메소드의 최종 반환 값인 Promise의 이행을 기다린다.

    마치 다음 코드와 같은 Promise Chaining을 reduce 메소드를 통해 구현한 것이다.

      await Promise.resolve()
        .then(() => handleTask(1))
        .then(() => handleTask(2))
        .then(() => handleTask(3))
        .then(() => handleTask(4))
        .then(() => handleTask(5))

    물론 reduce 함수 내에 async await 키워드를 사용할 수도 있다.

    async function main() {
      const tasks = [1, 2, 3, 4, 5];
    
      await tasks.reduce(async (prevTask, currTask) => {
        await prevTask;
        return handleTask(currTask)
      }, Promise.resolve());
    }

    병렬 실행

    이번에는 태스크 간의 우선순위나 선행 관계가 없어서 5개의 태스크가 모두 완료되기만 하면 될 때 병렬적으로 실행하는 방법을 알아보자.

    forEach?

    앞서 시도했던 forEach를 이용한 방법은 태스크를 병렬적으로 실행한다고 볼 수 있다. 하지만 문제는 해당 컨텍스트 내에서 5개의 태스크가 모두 끝나는 것을 기다릴 수가 없다는 것이다. 우리의 main 함수는 forEach 메소드를 실행하고 난 뒤에 생성된 Promise들을 남겨두고 종료된다.

    Promise.all

    자바스크립트는 Promise.all 이라는 유용한 메소드를 제공한다(MDN 설명). 매개변수로는 배열을 받는데, 배열의 요소로는 Promise를 포함한 어떤 값도 들어갈 수 있다. Promise.all은 매개변수로 전달한 배열에 있는 모든 Promise가 이행되거나, 처음으로 거부되는 때에 종료된다.

    Promise.all의 return값 또한 Promise인데, 모든 Promise가 이행된 경우에는 배열에 담긴 Promise의 순서대로 이행된 값을 담은 배열로 이행되고, 거부된 경우에는 처음으로 거부된 이유로 거부된다.

    async function main() {
      const tasks = [1, 2, 3, 4, 5];
    
      const taskPromises = tasks.map(task => handleTask(task));
      const result = await Promise.all(taskPromises);
      console.log(result);
    }
    [ undefined, undefined, undefined, undefined, undefined ]

    배열 메소드인 map을 이용해서 5개의 task를 이행하는 Promise의 배열을 만들고, Promise.all로 Promise들의 배열을 실행하였다.

    main 함수 실행 1초 뒤에 task1~5 finished 로그 5개가 동시에 출력되고 main함수가 종료된다. 이때 handleTask Promise는 반환하는 값이 없기 때문에 result는 undefined가 5개 들어있는 배열이 된다.

    Conclusion

    비동기 태스크들을 순차적으로 실행할지, 병렬적으로 실행할지는 굉장히 중요한 문제이다. 해결해야 하는 목표에 따라서 순차적으로, 또는 병렬적으로, 올바른 방식으로 구현할 수 있어야 한다.

    반응형

    댓글

Designed by Tistory.