Thread Pool은 비동기 작업을 효율적으로 처리하기 위한 중요한 개념입니다. Node.js는 싱글 스레드로 동작하는 이벤트 기반 환경이지만, I/O 작업이나 CPU 집중 작업을 효율적으로 처리하기 위해 백그라운드에서 여러 스레드를 사용하는 방식을 채택하고 있습니다. 이 스레드 풀(Thread Pool)은 libuv 라이브러리에 의해 관리됩니다.
Node.js는 자바스크립트의 싱글 스레드 모델을 따르며, 메인 이벤트 루프에서 자바스크립트 코드를 처리합니다. 하지만, 파일 시스템, 네트워크, 암호화 작업 등과 같은 I/O 작업을 비동기적으로 처리할 때, 스레드를 사용하지 않고도 효율적으로 실행됩니다. 이러한 비동기 작업 중 일부는 스레드 풀을 통해 병렬로 처리됩니다.
Thread Pool은 Node.js에서 특정 작업을 비동기적으로 처리할 수 있도록 돕는 멀티 스레드 환경입니다. 주로 I/O 바운드 작업이나 CPU 집약적인 작업을 처리할 때 사용됩니다. 기본적으로 Node.js는 4개의 스레드로 구성된 풀을 사용하며, 이를 통해 자원 효율성을 높이고 동시 처리를 가능하게 합니다.
Node.js의 Thread Pool이 사용되는 주요 작업은 다음과 같습니다:
Worker Threads
모듈을 사용하면 스레드 풀을 직접 활용하여 복잡한 작업을 처리할 수 있습니다.기본적으로 Node.js의 스레드 풀은 4개의 스레드로 구성되어 있지만, 이를 UV_THREADPOOL_SIZE
환경 변수를 사용하여 최대 128개까지 늘릴 수 있습니다. 이 값을 조정하면 Node.js가 동시에 더 많은 작업을 처리할 수 있지만, 너무 크게 설정하면 오히려 성능이 저하될 수 있습니다.
스레드 풀 크기 설정
이 명령은 스레드 풀 크기를 8개로 설정하고 app.js
를 실행합니다. 이렇게 설정하면 8개의 스레드가 동시 작업을 처리할 수 있게 됩니다.
UV_THREADPOOL_SIZE=8 node app.js
아래 코드는 Node.js에서 Thread Pool의 크기를 확인하고, 스레드 풀 크기를 4개로 설정했을 때와 6개로 늘렸을 때 각각의 처리 시간을 비교하는 예제입니다.
const crypto = require('crypto');
const start = Date.now();
function encryptTask(id) {
crypto.pbkdf2('password', 'salt', 100000, 64, 'sha512', () => {
console.log(`Task ${id} 완료:`, Date.now() - start, 'ms');
});
}
// 4개의 작업을 실행 (기본 Thread Pool 크기인 4에 맞춰)
for (let i = 1; i <= 4; i++) {
encryptTask(i);
}
// 추가로 작업을 요청해 스레드가 어떻게 처리되는지 확인
encryptTask(5);
encryptTask(6);
Task 1
~ Task 4
)은 동시에 처리됩니다. 이후에 실행된 Task 5
와 Task 6
은 대기하게 되며, 첫 번째 그룹의 작업이 끝난 후에 처리됩니다. 이로 인해 추가 작업들이 처리 시간이 더 길어지는 현상이 발생합니다.스레드 풀의 사이즈를 6개로 늘린 뒤 위 코드를 실행해보겠습니다.
UV_THREADPOOL_SIZE=6 node app.js
fs.readFile
메서드는 Thread Pool을 사용하여 파일을 읽습니다. 메인 스레드(이벤트 루프)는 이 작업을 기다리지 않고 다른 작업을 처리할 수 있습니다. 파일을 다 읽으면 스레드 풀에서 결과를 이벤트 루프로 전달하고, 콜백 함수가 호출됩니다.
const fs = require('fs');
// 파일을 비동기적으로 읽음 (Thread Pool 사용)
fs.readFile('largeFile.txt', (err, data) => {
if (err) {
return console.error(err);
}
console.log('파일 내용:', data.toString());
});
Worker Threads
모듈을 사용하면 직접적으로 스레드를 생성하여 CPU 집약적인 작업을 처리할 수 있습니다. 이는 기본적으로 Thread Pool
과 비슷한 개념이지만, 더 복잡한 작업을 효율적으로 처리할 수 있도록 고안되었습니다.
const { Worker, isMainThread, parentPort } = require('worker_threads');
if (isMainThread) {
// 메인 스레드에서 Worker 생성
const worker = new Worker(__filename); // 현재 파일을 Worker로 사용
worker.on('message', (count) => {
console.log(`Worker 완료: ${count}`);
});
worker.on('error', (err) => {
console.error('Worker 에러:', err);
});
worker.on('exit', (code) => {
if (code !== 0) {
console.error(`Worker가 종료되었습니다. 종료 코드: ${code}`);
}
});
} else {
// Worker 스레드에서 실행될 코드
let count = 0;
for (let i = 0; i < 10000000; i++) count++;
// 결과를 메인 스레드로 전송
parentPort.postMessage(count);
}