- [Node.js] 대량 API 요청의 효율적인 처리: p-limit을 활용한 동시성 제한2025년 05월 01일 20시 58분 35초에 업로드 된 글입니다.작성자: DandyNow728x90반응형
Node.js에서 대량 API 요청의 효율적인 처리: p-limit을 활용한 동시성 제한
Node.js 애플리케이션에서 다수의 API 요청을 처리할 때 발생할 수 있는 문제점과 이를 해결하기 위한 최적화 방법이 본 글의 주제이다. 대량의 동시 요청은 서버에 과부하를 일으키고 자원을 비효율적으로 사용하게 하는 원인이다. 이러한 문제의 효과적인 해결책을 살펴보도록 하겠다.
1. 문제: 대량의 순차적 API 요청
다음과 같은 코드는 많은 항목에 대해 API 요청을 순차적으로 수행하는 예시이다:
const request = require('request'); async function fetchDataForAllItems(items) { const results = []; for (const item of items) { const data = await fetchDataForItem(item.id); results.push(data); } return results; } function fetchDataForItem(itemId) { return new Promise((resolve, reject) => { request(`https://api.example.com/items/${itemId}`, (error, response, body) => { if (error) { return reject(error); } try { const data = JSON.parse(body); resolve(data); } catch (e) { reject(e); } }); }); } // 100개 항목에 대한 데이터 조회 const items = Array.from({ length: 100 }, (_, i) => ({ id: i + 1 })); fetchDataForAllItems(items).then(console.log);
이 코드의 문제점은 다음과 같다:
- 모든 요청이 순차적으로 처리되어 총 실행 시간이 길어지는 점
- 대량의 항목이 있을 경우 비효율적인 점
- 오류 처리가 제한적인 점
- API 제공자 측 서버에 과부하를 줄 위험성이 있는 점
2. 개선 방법
2-1. Promise.all을 사용한 병렬 처리
가장 간단한 최적화는 모든 요청을 병렬로 처리하는 것이다:
async function fetchDataForAllItemsParallel(items) { const promises = items.map(item => fetchDataForItem(item.id)); return Promise.all(promises); }
하지만 이 방법은 항목 수가 많을 경우 다음과 같은 문제를 일으킬 수 있다:
- 서버에 과부하 발생
- 네트워크 및 시스템 리소스 고갈
- API 제공자의 요청 제한 초과
2-2. p-limit 모듈을 사용한 동시성 제한
p-limit은 Promise 기반 작업의 동시성을 제한하는데 최적화된 인기 있는 npm 모듈이다. 이 모듈을 사용하면 커스텀 리미터를 구현하는 복잡성 없이 간단하게 동시 실행 수를 제한할 수 있다.
먼저 p-limit 모듈을 설치해야 한다:
npm i p-limit@3.1.0
p-limit을 사용한 동시성 제한 구현은 다음과 같다:
const pLimit = require('p-limit'); const request = require('request'); function fetchDataForItem(itemId) { return new Promise((resolve, reject) => { request(`https://api.example.com/items/${itemId}`, (error, response, body) => { if (error) { return reject(error); } try { const data = JSON.parse(body); resolve(data); } catch (e) { reject(e); } }); }); } async function fetchDataForAllItemsLimited(items) { // 최대 10개의 동시 요청으로 제한 const limit = pLimit(10); // 각 항목에 대한 Promise를 생성하되, p-limit으로 동시성 제한 const promises = items.map(item => limit(() => fetchDataForItem(item.id)) ); // 모든 Promise가 완료될 때까지 대기 return Promise.all(promises); } // 사용 예시 const items = Array.from({ length: 100 }, (_, i) => ({ id: i + 1 })); fetchDataForAllItemsLimited(items) .then(results => console.log(`처리된 항목 수: ${results.length}`)) .catch(error => console.error('오류 발생:', error));
이 방식의 장점은 다음과 같다:
- 코드가 간결하고 이해하기 쉽다
- 널리 사용되는 검증된 라이브러리를 활용한다
- 커스텀 구현의 잠재적 버그를 방지한다
- 동시성 제한 기능 외에도 추가 기능을 제공한다
2-3. 배치 처리 및 기타 최적화와 p-limit 결합
p-limit을 사용하면서 추가 최적화를 적용한 종합적인 솔루션은 다음과 같다:
const request = require('request'); const pLimit = require('p-limit'); // Promise 래핑된 request function requestPromise(options) { return new Promise((resolve, reject) => { request(options, (error, response, body) => { if (error) return reject(error); if (!response) return reject(new Error('No response')); if (response.statusCode < 200 || response.statusCode >= 300) { return reject(new Error(`HTTP Error: ${response.statusCode}`)); } try { const data = JSON.parse(body); resolve(data); } catch (e) { reject(new Error(`Parse error: ${e.message}`)); } }); }); } async function processItems(items, concurrency = 10, batchSize = 50) { // p-limit 인스턴스 생성 const limit = pLimit(concurrency); const results = []; // 타임아웃 설정 const timeoutMs = 5000; // 재시도 함수 const fetchWithRetry = async (itemId, retries = 3) => { try { return await limit(() => requestPromise({ url: `https://api.example.com/items/${itemId}`, timeout: timeoutMs }) ); } catch (error) { if (retries > 0) { const delay = 1000 * (4 - retries); // 재시도 전 지연 시간 await new Promise(r => setTimeout(r, delay)); return fetchWithRetry(itemId, retries - 1); } throw error; } }; // 배치 처리 for (let i = 0; i < items.length; i += batchSize) { const batch = items.slice(i, i + batchSize); // 각 배치 병렬 처리 const batchPromises = batch.map(item => fetchWithRetry(item.id) .then(data => ({ success: true, data, itemId: item.id })) .catch(error => ({ success: false, error: error.message, itemId: item.id })) ); const batchResults = await Promise.all(batchPromises); results.push(...batchResults); // 배치 간 지연 (선택적) if (i + batchSize < items.length) { await new Promise(r => setTimeout(r, 500)); } } return { successful: results.filter(r => r.success), failed: results.filter(r => !r.success) }; } // 사용 예시 async function main() { const items = Array.from({ length: 200 }, (_, i) => ({ id: i + 1 })); console.time('API 요청 처리'); const result = await processItems(items, 10, 50); console.timeEnd('API 요청 처리'); console.log(`성공: ${result.successful.length}, 실패: ${result.failed.length}`); if (result.failed.length > 0) { console.log('실패한 항목:', result.failed.map(f => f.itemId)); } } main().catch(console.error);
3. 주요 개선 요소 및 기법
p-limit을 활용한 동시성 제한
- 검증된 라이브러리로 동시에 실행되는 요청 수 제한
- 간결한 API로 쉽게 구현 가능
- 과도한 리소스 사용과 서버 부하 방지
배치 처리
- 전체 항목을 작은 배치로 나누어 처리
- 메모리 사용량 제어 및 진행 상황 관리가 용이한 장점
- 중간 결과를 확인할 수 있는 이점
재시도 메커니즘
- 일시적인 오류에 대응하는 지수 백오프 재시도 전략
- 네트워크 불안정성에 대한 견고성 향상 효과
- 성공률을 높이는 핵심 요소
타임아웃 설정
- 응답 없는 요청이 리소스를 계속 점유하는 것을 방지하는 안전장치
- 전체 작업 시간 예측 가능성 향상에 기여
- 좀비 요청 방지 효과
에러 처리 및 로깅
- 성공 및 실패 결과 구분으로 명확한 결과 분석 가능
- 오류 원인 추적 및 분석이 용이한 구조
- 결과 보고서 생성이 간편한 이점
4. p-limit 활용의 추가 이점
p-limit 라이브러리는 다음과 같은 추가 이점을 제공한다:
큐 관리 자동화
- 내부적으로 대기 큐를 최적으로 관리하는 기능
- 메모리 누수 방지 메커니즘 내장
간단한 API
- 복잡한 동시성 제어를 단 몇 줄의 코드로 구현 가능
- 유지보수가 쉽고 코드 가독성이 높은 장점
성능 최적화
- 내부적으로 최적화된 알고리즘 사용
- 오버헤드가 적은 효율적인 구현
메타 정보 접근
- 활성 Promise 수, 대기 중인 작업 수 등 정보 접근 가능
- 동적 모니터링과 조정이 가능한 이점
5. 결론
대량의 API 요청을 효율적으로 처리하기 위해서는 단순히 순차적 또는 완전 병렬 처리보다 더 세련된 접근 방식이 필요하다. p-limit과 같은 라이브러리를 활용한 동시성 제한, 배치 처리, 재시도 전략 등의 기법을 조합하면 시스템 리소스를 효율적으로 사용하면서도 전체 성능을 크게 향상시킬 수 있다.
특히 대규모 프로덕션 환경에서는 다음과 같은 추가 고려사항이 중요하다:
- 모니터링 및 로깅: 성능 지표 수집 및 병목 지점 식별이 필수 요소이다
- 캐싱: 중복 요청 제거 및 응답 시간 단축의 핵심 전략이다
- 서킷 브레이커 패턴: 장애가 검출된 서비스에 대한 요청 차단 메커니즘이다
- 부하 테스트: 최적의 동시성 한계 설정을 위한 테스트는 필수적이다
이러한 최적화 기법을 적용하면 더 안정적이고 효율적인 Node.js 애플리케이션을 구축할 수 있다. p-limit과 같은 검증된 라이브러리의 활용은 개발 시간을 단축하고 코드 품질을 향상시키는 현명한 선택이다.
728x90반응형'언어·프레임워크 > Node.js' 카테고리의 다른 글
[Node.js] 배치 프로그램에 강력한 로깅, Winston 적용하기 (1) 2025.06.04 [Node.js] cron-cluster: 분산 환경에서의 크론 작업 관리 이해하기 (0) 2025.04.11 [Node.js] Redis 데이터베이스를 활용한 Node.js cron-cluster 동시 실행 문제 해결하기 (0) 2025.04.11 [Node.js] date-fns 모듈로 크론 작업의 날짜 계산 오류 방지하기 (0) 2025.03.21 [Node.js] "객체에서 배열 VS 배열에서 객체" 변환, 누가 더 빠를까? (0) 2025.02.27 다음글이 없습니다.이전글이 없습니다.댓글