Node.js 이벤트 루프와 Promise 동시성 제어

문제 상황

외부 API에서 1000개의 상품 정보를 가져오는 크롤러를 작성했다. Promise.all로 모든 요청을 한번에 처리했더니 상대 서버에서 429 에러를 반환하며 IP를 차단했다.

// 문제가 있던 코드
const productIds = Array.from({length: 1000}, (_, i) => i);
const results = await Promise.all(
  productIds.map(id => fetchProduct(id))
);

동시성 제어 구현

한번에 처리할 작업 수를 제한하는 함수를 작성했다. Promise를 청크 단위로 나눠서 순차적으로 처리하는 방식이다.

async function promiseConcurrency(tasks, limit) {
  const results = [];
  for (let i = 0; i < tasks.length; i += limit) {
    const chunk = tasks.slice(i, i + limit);
    const chunkResults = await Promise.all(chunk.map(task => task()));
    results.push(...chunkResults);
  }
  return results;
}

// 사용
const tasks = productIds.map(id => () => fetchProduct(id));
const results = await promiseConcurrency(tasks, 10);

더 정교한 제어가 필요해서 p-limit 라이브러리도 검토했다.

const pLimit = require('p-limit');
const limit = pLimit(10);

const results = await Promise.all(
  productIds.map(id => limit(() => fetchProduct(id)))
);

이벤트 루프 이해

Node.js는 싱글 스레드지만 libuv가 I/O 작업을 비동기로 처리한다. Promise.all은 모든 Promise를 즉시 시작하므로, 네트워크 요청이 동시에 발생한다. 이를 제어하려면 Promise 생성 시점을 지연시켜야 한다.

결과

동시 요청 수를 10개로 제한하자 안정적으로 동작했다. 처리 시간은 늘었지만 차단 없이 모든 데이터를 수집할 수 있었다. 외부 API 호출 시에는 항상 Rate Limiting을 고려해야 한다는 교훈을 얻었다.