
노드JS에서 멀티스레딩 완벽 정복! 성능 향상의 비밀 풀기
노드JS(Node.js)는 기본적으로 싱글 스레드(Single-Threaded)로 동작합니다.
즉, 논블로킹 I/O 작업을 처리하는 데는 매우 효율적이지만, CPU 집약적인 작업(CPU-intensive tasks)을 수행할 때는 성능 병목 현상이 발생할 수 있습니다.
이를 해결하기 위해 노드JS는 여러 스레드를 활용할 수 있는 다양한 방법을 제공합니다.
이번 글에서는 왜 멀티스레딩이 필요하고, 이를 구현하는 3가지 주요 방법(Child Process, Worker Threads, Cluster)을 자세히 살펴보겠습니다.
1. 왜 노드JS에서 서브 스레드가 필요한가요?
노드JS는 이벤트 루프(Event Loop) 기반의 싱글 스레드 모델로 설계됐습니다.
덕분에 파일 읽기, 네트워크 요청과 같은 논블로킹 I/O 작업은 매우 효율적으로 처리됩니다.
하지만 대규모 계산 작업과 같은 CPU 집약적인 작업은 이벤트 루프를 차단해 전체 애플리케이션 성능을 저하시킵니다.
서브 스레드를 활성화하면 다음과 같은 문제를 해결할 수 있는데요.
- 논블로킹 유지: 메인 스레드에서 외부 명령어를 직접 실행하면 이벤트 루프가 차단될 수 있습니다. 서브 스레드에서 이를 처리하면 메인 스레드는 여전히 비동기 작업을 처리할 수 있습니다.
- 멀티코어 CPU 활용:
child_process나worker_threads를 사용하면 여러 CPU 코어에서 병렬 작업을 수행할 수 있습니다. 특히 무거운 계산 작업에 유리합니다. - 격리 및 보안 강화: 서브 스레드에서 작업을 분리하면, 해당 작업에서 오류가 발생하더라도 메인 프로세스가 영향을 받지 않아 애플리케이션 안정성이 높아집니다.
- 유연한 데이터 처리: IPC(Inter-Process Communication)를 통해 작업 결과를 메인 프로세스로 안전하게 전달할 수 있습니다.
간단히 말해, 싱글 스레드의 한계를 극복하고 CPU 자원을 최대한 활용하기 위해 멀티스레딩이 필요합니다.
2. 자식 프로세스(Child Process)로 멀티스레딩 구현하기
child_process 모듈은 외부 명령어 실행이나 독립적인 Node.js 모듈 실행을 위해 사용됩니다.
주요 메서드로는 spawn(), exec(), fork()가 있는데요, 각각의 특징을 알아보겠습니다.
spawn(): 스트림 방식으로 데이터 처리
spawn()은 장시간 실행되는 명령어나 대용량 출력을 처리할 때 적합합니다.
데이터를 스트림으로 처리하기 때문에 메모리 부담이 적습니다.
기본 문법:
const { spawn } = require('child_process');
const child = spawn(command, [args], [options]);
command: 실행할 명령어 (예:touch,ls)args: 명령어 인자 배열 (예:['moment.txt'])options: 작업 디렉토리(cwd), 환경 변수(env) 등을 설정 가능
예제:
const { spawn } = require('child_process');
const path = require('path');
const touch = spawn('touch', ['moment.txt'], {
cwd: path.join(process.cwd(), './m'), // 현재 디렉토리 아래 m 폴더에서 실행
});
touch.on('close', (code) => {
if (code === 0) {
console.log('파일 생성 성공!');
} else {
console.error(`에러 발생, 종료 코드: ${code}`);
}
});
위 코드는 ./m 디렉토리에 moment.txt 파일을 비동기로 생성합니다.
exec(): 결과값을 메모리에 버퍼링
exec()은 출력 데이터가 작은 경우 유용합니다.
모든 결과를 메모리에 저장하기 때문에 대용량 데이터에는 적합하지 않습니다.
기본 문법:
const { exec } = require('child_process');
exec(command, [options], callback);
callback:(error, stdout, stderr)를 반환
예제:
const { exec } = require('child_process');
const path = require('path');
const command = `touch ${path.join('./m', 'moment.txt')}`;
exec(command, { cwd: process.cwd() }, (error, stdout, stderr) => {
if (error) {
console.error(`명령어 실행 실패: ${error}`);
return;
}
if (stderr) {
console.error(`에러 출력: ${stderr}`);
return;
}
console.log('파일 생성 완료!');
});
spawn()과 달리 exec()은 간단한 작업에 적합합니다.
fork(): 독립적인 Node.js 프로세스 생성
fork()는 child_process 중에서도 오직 Node.js 전용입니다.
별도의 V8 인스턴스를 실행하며 IPC(프로세스 간 통신)를 지원합니다.
기본 문법:
const { fork } = require('child_process');
const child = fork(modulePath, [args], [options]);
예제 (상위 프로세스: index.js):
const { fork } = require('child_process');
const child = fork('./child.js');
child.on('message', (msg) => {
console.log('자식 프로세스로부터 메시지:', msg);
});
child.send({ hello: 'world' });
자식 프로세스 (child.js):
process.on('message', (msg) => {
console.log('부모 프로세스로부터 메시지:', msg);
process.send({ foo: 'bar' });
});
이렇게 fork()를 사용하면 별도의 Node.js 인스턴스가 실행되며, 메시지 이벤트로 자유롭게 데이터를 주고받을 수 있습니다.
3. Worker Threads로 효율적인 병렬 처리
worker_threads는 하나의 프로세스 내에서 여러 스레드를 병렬로 실행할 수 있게 해줍니다.
별도의 프로세스를 만들지 않기 때문에 메모리 공유가 가능하고 속도도 빠릅니다.
핵심 개념:
- Worker: 독립적인 V8 인스턴스에서 실행되는 자바스크립트 스레드
- Main Thread: 최초의 Node.js 이벤트 루프가 동작하는 메인 스레드
- 통신 방식:
postMessage()와parentPort로 데이터를 주고받음
예제:
const { Worker, isMainThread, parentPort } = require('worker_threads');
if (isMainThread) {
const worker = new Worker(__filename);
worker.on('message', (msg) => console.log('워커로부터:', msg));
worker.postMessage('안녕, 워커!');
} else {
parentPort.on('message', (msg) => {
console.log('메인 스레드로부터:', msg);
parentPort.postMessage('반가워, 메인!');
});
}
이 코드는 같은 파일 내에서 메인 스레드와 워커 스레드를 구분해 실행합니다.
worker_threads vs fork():
worker_threads: 가벼운 병렬 처리, 메모리 공유 가능 (CPU 집약적 작업에 적합)fork(): 완전한 독립 프로세스 생성, 메모리 중복 발생 (격리된 작업에 적합)
4. Cluster 모듈로 멀티코어 서버 활용하기
cluster 모듈은 여러 CPU 코어에서 Node.js 서버를 동시에 실행할 수 있게 해줍니다.
특히 HTTP 서버 확장에 유용합니다.
작동 원리:
- 마스터 프로세스가 여러 워커 프로세스를 생성
- 각 워커는 동일한 서버 코드를 실행 (포트 공유 가능)
- 마스터는 요청을 워커들에게 분배
예제:
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
console.log(`마스터 프로세스 ${process.pid} 실행 중`);
for (let i = 0; i < numCPUs; i++) {
cluster.fork(); // 워커 생성
}
cluster.on('exit', (worker) => {
console.log(`워커 ${worker.process.pid} 종료됨`);
});
} else {
http.createServer((req, res) => {
res.end('Hello World\n');
}).listen(8000);
console.log(`워커 ${process.pid}가 서버 실행 중`);
}
이렇게 하면 여러 코어에서 병렬로 요청을 처리하므로 서버 성능이 비약적으로 향상됩니다.
5. 정리: 무엇을 선택할까?
- CPU 집약적 계산:
worker_threads - 외부 명령 실행:
child_process.spawn()또는exec() - 독립적인 Node.js 작업:
fork() - 서버 확장 및 부하 분산:
cluster
노드JS는 싱글 스레드지만, 멀티스레딩을 적절히 활용하면 성능 한계를 뛰어넘을 수 있습니다.
'Javascript' 카테고리의 다른 글
| 플레이라이트(Playwright) vs 퍼펫티어(Puppeteer): 지금 갈아타야 할까요? (마이그레이션 가이드) (0) | 2025.05.05 |
|---|---|
| 자바스크립트 샌드박스 완벽 파헤치기: 안전한 코드 실행 환경 만들기 (0) | 2025.05.05 |
| 타입스크립트 infer 키워드 완벽 정복 가이드 (0) | 2025.04.27 |
| Node.js 성능의 비밀! libuv와 함께 CPU/IO 바운드 완벽 정복 가이드 (1) | 2025.04.26 |
| CSS 로딩이 DOM 파싱과 렌더링에 미치는 영향 (1) | 2025.04.25 |