프로미스 병렬 처리 심화 (Advanced Promise Parallelism)

프로미스 병렬 처리 심화 (Advanced Promise Parallelism)

자바스크립트에서 여러 비동기 작업을 동시에 처리하려면 프로미스 병렬 처리가 필수다. 순차적으로 기다리는 대신 병렬로 실행하면 시간도 줄고 효율도 올라간다. 이번에는 프로미스 병렬 처리의 기본부터 심화 기법까지 코드와 함께 자세히 파헤쳐보자.


단계별로 하나씩 풀어보면서 다양한 상황에서 어떻게 활용할 수 있는지 알아보자.


프로미스 병렬 처리 기본

Promise.all은 여러 프로미스를 병렬로 실행하고 모두 완료될 때까지 기다린다:

function delay(ms, value) {
    return new Promise((resolve) => {
        setTimeout(() => resolve(value), ms);
    });
}

const promises = [
    delay(1000, "첫 번째"),
    delay(2000, "두 번째"),
    delay(1500, "세 번째")
];

Promise.all(promises)
    .then(results => {
        console.log(results);
    });
// 2초 후 ["첫 번째", "두 번째", "세 번째"]

가장 긴 지연 시간(2초) 후에 결과가 한꺼번에 나온다. 순차 처리였다면 4.5초가 걸렸을 텐데 병렬로 하니 훨씬 빠르다.


1. 병렬 API 호출

여러 API 요청을 병렬로 처리해보자:

async function fetchPost(id) {
    const response = await fetch(
        `https://jsonplaceholder.typicode.com/posts/${id}`
    );
    return response.json();
}

const postIds = [1, 2, 3];
const promises = postIds.map(id => fetchPost(id));

Promise.all(promises)
    .then(posts => {
        posts.forEach(post => console.log(post.title));
    });
// 병렬로 호출 후 결과 출력
// "sunt aut facere repellat provident occaecati excepturi optio reprehenderit"
// "qui est esse"
// "ea molestias quasi exercitationem repellat qui ipsa sit aut"

각 요청이 독립적으로 실행되고, 가장 느린 요청이 끝날 때까지 기다린 후 결과를 한꺼번에 받았다.


2. 에러 처리와 Promise.all

Promise.all은 한 프로미스가 실패하면 전체가 실패한다:

const promises = [
    delay(1000, "성공"),
    Promise.reject(new Error("실패")),
    delay(2000, "늦은 성공")
];

Promise.all(promises)
    .then(results => console.log(results))
    .catch(error => console.log("에러: " + error.message));
// "에러: 실패"

하나라도 실패하면 즉시 에러가 발생하고 나머지 프로미스는 무시된다.


3. Promise.allSettled로 실패 무시

Promise.allSettled는 모든 프로미스가 완료될 때까지 기다리며 성공/실패 상태를 반환한다:

const promises = [
    delay(1000, "성공"),
    Promise.reject(new Error("실패")),
    delay(2000, "늦은 성공")
];

Promise.allSettled(promises)
    .then(results => {
        results.forEach(result => {
            if (result.status === "fulfilled") {
                console.log("성공: " + result.value);
            } else {
                console.log("실패: " + result.reason.message);
            }
        });
    });
// "성공: 성공"
// "실패: 실패"
// "성공: 늦은 성공"

실패해도 다른 프로미스 결과를 잃지 않고 모두 확인할 수 있다.


4. 병렬 실행 제어 (Concurrency Control)

너무 많은 프로미스를 한꺼번에 실행하면 리소스가 과부하될 수 있다. 제한된 병렬 실행을 구현해보자:

async function limitParallel(tasks, limit) {
    const results = new Array(tasks.length);
    const executing = [];

    async function runTask(task, index) {
        try {
            const result = await task();
            results[index] = { status: "fulfilled", value: result };
        } catch (error) {
            results[index] = { status: "rejected", reason: error };
        } finally {
            const indexToRemove = executing.indexOf(promise);
            executing.splice(indexToRemove, 1);
            if (queue.length) {
                executing.push(queue.shift()());
            }
        }
    }

    const queue = [...tasks.entries()].map(([i, task]) => () => runTask(task, i));

    for (let i = 0; i < limit && queue.length; i++) {
        const promise = queue.shift()();
        executing.push(promise);
    }

    while (executing.length || queue.length) {
        await Promise.race(executing);
    }

    return results;
}

const tasks = [
    () => delay(1000, "작업 1"),
    () => delay(2000, "작업 2"),
    () => delay(1500, "작업 3"),
    () => delay(3000, "작업 4"),
    () => delay(500, "작업 5")
];

limitParallel(tasks, 2)
    .then(results => {
        results.forEach(r => console.log(r.value));
    });
// 최대 2개씩 병렬 실행
// "작업 1", "작업 5", "작업 3", "작업 2", "작업 4" 순으로 출력

동시에 실행되는 작업 수를 2개로 제한하면서도 모든 작업을 효율적으로 완료했다.


5. Promise.race와 경쟁

Promise.race는 가장 먼저 완료된 프로미스의 결과를 반환한다:

const promises = [
    delay(1000, "빠른 작업"),
    delay(2000, "느린 작업"),
    delay(1500, "중간 작업")
];

Promise.race(promises)
    .then(result => console.log(result));
// 1초 후 "빠른 작업"

가장 빠른 프로미스만 결과를 주고 나머지는 무시된다.


6. 복잡한 병렬 작업 조합

API 호출과 계산 작업을 병렬로 조합해보자:

async function fetchUser(id) {
    const response = await fetch(
        `https://jsonplaceholder.typicode.com/users/${id}`
    );
    return response.json();
}

async function computeHeavyTask() {
    return new Promise(resolve => {
        let sum = 0;
        for (let i = 0; i < 1e7; i++) sum += i;
        resolve(sum);
    });
}

async function processCombined() {
    const results = await Promise.all([
        fetchUser(1),
        fetchUser(2),
        computeHeavyTask()
    ]);
    console.log("유저 1: " + results[0].name);
    console.log("유저 2: " + results[1].name);
    console.log("계산 결과: " + results[2]);
}

processCombined();
// "유저 1: Leanne Graham"
// "유저 2: Ervin Howell"
// "계산 결과: 49999995000000"

네트워크 요청과 CPU 작업을 병렬로 처리하며 결과를 한 번에 받았다.


7. 타임아웃과 병렬 처리

특정 시간이 지나면 타임아웃을 적용해보자:

function withTimeout(promise, ms) {
    const timeout = new Promise((_, reject) =>
        setTimeout(() => reject(new Error("타임아웃")), ms)
    );
    return Promise.race([promise, timeout]);
}

const promises = [
    withTimeout(delay(1000, "빠름"), 1500),
    withTimeout(delay(2000, "느림"), 1500)
];

Promise.all(promises)
    .then(results => console.log(results))
    .catch(error => console.log(error.message));
// "타임아웃" (2초 작업은 1.5초 제한을 초과)

시간 제한을 두어 느린 작업을 실패 처리했다.


8. 병렬 스트림 처리

데이터 스트림을 병렬로 처리해보자:

async function processChunk(chunk) {
    return delay(1000, `처리된 ${chunk}`);
}

async function streamParallel(data, batchSize) {
    const results = [];
    for (let i = 0; i < data.length; i += batchSize) {
        const batch = data.slice(i, i + batchSize);
        const batchResults = await Promise.all(
            batch.map(item => processChunk(item))
        );
        results.push(...batchResults);
    }
    return results;
}

const data = ["a", "b", "c", "d", "e"];
streamParallel(data, 2)
    .then(results => console.log(results));
// ["처리된 a", "처리된 b", "처리된 c", "처리된 d", "처리된 e"]

데이터를 배치로 나눠 병렬 처리하며 순차적 흐름과 병렬의 장점을 결합했다.


9. 병렬 처리 성능 분석

병렬 처리가 성능에 어떤 영향을 주는지 살펴보자:

- 속도: 작업이 독립적일수록 병렬 실행으로 시간이 단축된다.

- 리소스: 너무 많은 병렬 작업은 메모리와 네트워크를 과부하할 수 있다.

Promise.all은 단순 병렬에 강력하지만, allSettled나 concurrency 제어로 유연성을 더할 수 있다.


마무리

프로미스 병렬 처리는 비동기 작업을 효율적으로 관리하는 강력한 도구다. 기본적인 Promise.all부터 allSettled, concurrency 제어, 타임아웃, 스트림 처리까지 다양한 상황에 맞춰 활용할 수 있다.


+ Recent posts