Promise를 이용한 비동기 순차 처리와 병렬 처리 핸들하기

JavaScript에서 비동기 작업을 처리할 때, 다양한 처리 방식을 사용할 수 있다. 이 글에서는 JavaScript의 Array.prototype.reduce, Promise.all 그리고 Promise.allSettled 메서드를 사용하여 순차 처리와 병렬 처리를 어떻게 구현할 수 있는지 살펴보자.

병렬 처리: Promise.all 및 Promise.allSettled

Promise.all과 Promise.allSettled를 사용하면 프로미스를 병렬로 실행할 수 있다. 이 방식은 독립적인 작업에 적합하며, 각 프로미스의 처리 시간이 가장 긴 것에 따라 전체 소요 시간이 결정된다.

Promise.all

Promise.all은 모든 프로미스가 성공적으로 완료될 때까지 기다린 후 결과 배열을 반환한다. 하나의 프로미스라도 실패하면 전체가 실패로 간주되며, 개별 프로미스의 성공 또는 실패를 추적할 수 없다.
이어서 소개할 Promise.allSettled를 사용하면, 각 프로미스의 성공과 실패 여부를 확인하고 각각의 결과를 처리할 수 있다.

먼저 Promise.all의 예시를 보면,

const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => { resolve('성공1'); console.log('성공1'); }, 1000);
});

const promise2 = new Promise((resolve, reject) => {
  setTimeout(() => { reject('실패2'); console.log('실패2'); }, 2000);
});

const promise3 = new Promise((resolve, reject) => {
  setTimeout(() => { resolve('성공3'); console.log('성공3'); }, 3000);
});

Promise.all([promise1, promise2, promise3])
  .then((results) => {
    console.log(results);
  })
  .catch((error) => {
    console.error(`실패: ${error}`);
  });
성공1
실패2 // Promise 결과 
성공3

promise2가 실패하면 Promise.all이 즉시 실패한다. 그러나 promise1과 promise3은 여전히 실행되며 각각의 프로미스는 성공하거나 실패할 수 있다. 다만 추적하지 않을 뿐이다. 이러한 동작을 원하지 않는 경우, 앞서 언급한 Promise.allSettled를 사용하여 모든 프로미스의 결과를 얻을 수 있다.

Promise.all 사용시 멱등성 주의

멱등성(idempotence)이란, 연산을 여러 번 수행해도 결과가 달라지지 않는 성질을 말한다. 즉, 같은 요청을 여러 번 보내도 동일한 결과가 반환되는 경우 멱등성이 있는 것이다. Promise.all은 여러 프로미스를 병렬로 실행하고 모든 프로미스가 성공적으로 완료될 때 결과 배열을 반환하는 데 유용하다. 그러나 Promise.all은 하나의 프로미스가 실패하면 전체가 실패하는 방식으로 동작하기 때문에, 멱등성이 중요한 상황에서는 사용에 주의해야 한다.

멱등성이 보장되는 경우에는 Promise.all을 사용해도 괜찮다. 예를 들어, 여러 API 요청을 병렬로 수행하고자 하는 경우에 해당 API가 멱등성을 보장한다면, 요청이 실패한 경우에도 다시 시도할 수 있습니다. 그러나 멱등성이 보장되지 않는 경우, 실패한 프로미스로 인해 다른 프로미스들이 중단되어서는 안 되기 때문에 Promise.all을 사용하는 것이 바람직하지 않다. 이러한 경우, 각각의 프로미스 결과를 추적하고 싶다면 Promise.allSettled를 사용하거나, 순차적으로 처리하고자 하는 경우 for 루프 또는 Array.prototype.reduce를 사용하여 프로미스 체인을 구성할 수 있다.

Promise.allSettled

Promise.allSettled는 각 프로미스가 성공 또는 실패한 후에 결과를 반환한다. 이를 통해 개별 프로미스의 상태를 추적하고 실패한 프로미스를 별도로 처리할 수 있다. 이 방법은 서로 종속되지 않은 독립적인 작업이 있을 때 유용하다.

const promise1 = Promise.resolve('성공1');
const promise2 = Promise.resolve('성공2');
const promise3 = Promise.reject('실패1');
const promise4 = Promise.reject('실패2');

Promise.allSettled([promise1, promise2, promise3, promise4])
    .then((results) => {
        results.forEach((result) => {
            if (result.status === 'fulfilled') {
                console.log(`성공: ${result.value}`);
            } else if (result.status === 'rejected') {
                console.log(`실패: ${result.reason}`);
            }
        });
    });
성공: 성공1
성공: 성공2
실패: 실패1
실패: 실패2

최대 소요 시간

Promise.all과 Promise.allSettled는 병렬로 여러 프로미스를 실행하기 때문에, 최대 소요 시간은 처리 시간이 가장 긴 프로미스의 시간이 된다. 예를 들어, 3개의 프로미스가 각각 1초, 2초, 3초가 걸린다고 가정해보면, Promise.all 또는 Promise.allSettled를 사용하여 이러한 프로미스를 병렬로 실행하면, 가장 긴 실행 시간인 3초가 걸린 프로미스가 완료되는 시점에서 모든 프로미스가 완료된 것으로 간주된다. 따라서 최대 소요 시간은 가장 긴 프로미스의 시간인 3초가 된다.

최대 소요 시간 3초

순차 처리: Array.prototype.reduce

앞서 살펴본 Promise.all과 Promise.allSettled는 Promise들이 동시 병렬처리 되므로 Promise간의 개연성이 없는 경우에 적합하다. 만약 Promise 관계가 선후행이 있다면 그 경우에는 사용이 적헙하지 않다. 대신 선후행 관계가 필요하다면 Array.prototype.reduce를 사용하면 프로미스를 순차적으로 실행할 수 있다. 이 방식은 프로미스 간에 종속성이 있거나 순서가 중요한 작업에 적합하다.

다음은 Array.prototype.reduce를 사용하여 순차적으로 프로미스를 실행하는 예제이다.

const promiseFunction1 = () => new Promise((resolve) => {
  setTimeout(() => {
    console.log('첫 번째 작업 완료');
    resolve('성공1');
  }, 1000);
});

const promiseFunction2 = () => new Promise((resolve, reject) => {
  setTimeout(() => {
    console.log('두 번째 작업 실패');
    reject('실패2');
  }, 2000);
});

const promiseFunction3 = () => new Promise((resolve) => {
    setTimeout(() => {
        console.log('세 번째 작업 완료');
        resolve('성공3');
    }, 3000);
});

const promiseFunctions = [promiseFunction1, promiseFunction2, promiseFunction3];

promiseFunctions.reduce((accumulator, currentFunction) => {
    return accumulator
        .then(() => currentFunction())
        .catch((error) => {
            console.error(`작업 실패: ${error}`);
            throw error;
        });
}, Promise.resolve())
    .then(() => {
        console.log('모든 작업이 완료되었습니다.');
    })
    .catch((error) => {
        console.error(`중단된 작업: ${error}`);
    });
첫 번째 작업 완료
두 번째 작업 실패
중단된 작업: 실패2

이 예제에서는 promiseFunction2가 실패한다. 중간 프로미스가 실패하는 경우, catch() 를 사용하여 오류를 처리하고, 다음 프로미스 실행 여부를 결정할 수 있다. 중간에서 실패하는 경우 이후 프로미스를 처리하고 싶지 않다면, catch()를 사용하지 않고 오류를 계속 전파하면 된다.

결론

Array.prototype.reduce를 이용한 방식과 Promise.all/Promise.allSettled 방식은 프로미스 처리에 있어 중요한 차이점을 가지고 있다. 어떤 방식을 선택할지 결정하기 전에 각 방식의 특성을 이해하고 사용 사례를 고려해야 한다.

어떤 상황에서 어떤 방식을 선택할지 결정하는 요소는 다음과 같다.

  1. 순차적 실행이 필요한지, 병렬 실행이 가능한지
  2. 실패한 프로미스에 대해 별도의 오류 처리가 필요한지, 아니면 모든 결과를 한 번에 처리하려는지
  3. 실패한 프로미스로 인해 다른 프로미스들의 실행이 중단되어야 하는지, 아니면 독립적으로 실행되어야 하는지

Array.prototype.reduce

  1. 순차 처리
    • 프로미스가 순차적으로 실행된다. 즉, 이전 프로미스가 완료되면 다음 프로미스가 실행된다.
    • 이 방식은 종속성이 있는 작업에 적합하지만, 병렬 처리가 가능한 독립적인 작업에는 비효율적일 수 있다.
  2. 오류 처리
    • 중간 프로미스가 실패할 경우, .catch()를 사용하여 오류를 처리하고 다음 프로미스 실행 여부를 선택할 수 있다.
    • 일련의 프로미스가 순차적으로 실행되어야 하거나 실패한 프로미스를 처리하고 다음 프로미스를 실행할지 결정해야 하는 경우에 적합하다.

Promise.all

  1. 병렬 처리
    • Promise.all은 병렬 실행이므로 순차적 실행이 필요한 작업에는 적합하지 않다.
  2. 오류 처리
    • 실패 시 전체 실패
    • Promise.all은 하나의 프로미스가 실패하면 전체가 실패로 간주된다. 이러한 특성 때문에 개별 프로미스의 성공 또는 실패를 추적할 수 없다.

Promise.allSettled

  1. 병렬 처리
    • Promise.allSettled 역시 병렬 실행이므로, 모든 프로미스가 동시에 실행되고 각각의 프로미스는 독립적으로 완료된다.
    • 프로미스들이 독립적으로 실행되어야 하고, 각각의 프로미스가 성공하거나 실패한 결과를 추적하려는 경우에 적합하다.
  2. 결과 추적
    • 모든 프로미스가 완료되면 결과를 반환하고, 각 결과 객체에는 상태(status)와 값(value) 또는 거절 이유(reason)가 포함된다.
    • Promise.allSettled는 각 프로미스의 성공 또는 실패 결과를 개별적으로 추적할 수 있지만, 실패한 프로미스를 별도로 처리하는 데 추가 작업이 필요하다.

작업의 특성과 요구 사항에 따라 적절한 메서드를 선택하여 비동기 작업을 처리할 수 있다. 순차 처리와 병렬 처리의 차이를 이해하고, 작업의 요구 사항에 맞게 선택하는 것이 중요하다.



쿠팡 파트너스 활동을 통해 일정액의 커미션을 제공받을 수 있습니다.