비동기 작업 최적화: Promise.all이 성능을 해치는 경우와 루프 사용 시기
카테고리
프로그래밍/소프트웨어 개발
서브카테고리
웹 개발
대상자
- *대상자**: 중급 이상의 JavaScript/Node.js 개발자
- *난이도**: 실무에서 비동기 작업 성능 최적화에 관심 있는 개발자
핵심 요약
Promise.all
의 과도한 동시성은 데이터베이스 오버로드와 메모리 부족을 유발함 (50,000개 동시 요청 시 500개 동시 연결로 DB 충돌
)p-limit
를 사용한 제어된 동시성(10개 병렬 처리)으로 메모리 사용량 1.2GB → 200MB로 줄임- 메모리 제약 또는 순서 의존성이 있는 작업에는 루프가 더 안정적 (
for-await-of
, 배치 처리) Promise.allSettled
로 실패 처리를 개선한 예제 포함
섹션별 세부 요약
1. Promise.all의 성능 문제
- 50,000개 레코드 처리 시 500개 동시 연결로 DB 충돌 발생
await Promise.all(records.map(processRecord))
는 메모리와 동시성 제어 없음- 과도한 동시성으로 인한 DB 오버로드와 메모리 부족 문제 강조
2. p-limit로 동시성 제어
pLimit(10)
으로 최대 10개 병렬 작업 제한await Promise.all(hugeArray.map(item => limit(() => db.insert(item))))
로 메모리 사용량 200MB로 감소- 동시성 제어가 성능과 안정성 균형을 유지
3. Promise.allSettled로 실패 처리 개선
Promise.all
은 하나의 실패로 전체 작업 실패Promise.allSettled
+results.filter(r => r.status === 'fulfilled')
로 실패 별도 처리 가능
4. 스트리밍 처리로 메모리 최적화
for await (const record of getRecordsStream())
으로 레코드 하나씩 처리- 500MB 배열 메모리 대신 스트리밍으로 메모리 사용량 50MB로 감소
5. 시나리오 비교: Promise.all vs 루프
- 순서 의존성:
Promise.all
❌, 루프 ✅ - 메모리 제약:
Promise.all
❌, 루프 ✅ - 백프레셔:
Promise.all
❌, 루프 ✅
6. 하이브리드 접근 방식
for (let i = 0; i < items.length; i += batchSize)
로 배치 루프await Promise.all(batch.map(process))
로 제어된 동시성과 순서 보장- 시간/메모리:
Promise.all
2분 1.2GB →배치 루프
6분 50MB
결론
- "빠르다"는 항상 최선이 아님: 동시성 제어(
p-limit
)와 메모리 최적화(for-await-of
)가 핵심 - Hybrid Approach 권장: 루프 내부에서
Promise.all
배치 처리로 성능과 안정성 균형 달성 - 실무 팁:
Promise.all
은 작은 독립 작업에, 루프는 순서/메모리 제약 작업에 사용하며, 배치 처리로 동시성 제어하세요