콘텐츠로 이동

Node.js 성능 최적화 — Event Loop 병목부터 클러스터까지

Node.js는 단일 스레드 이벤트 루프 구조이기 때문에 CPU 집중 작업이 병목이 되면 모든 요청이 멈춥니다. 올바른 최적화 방법을 알아봅니다.

Node.js 이벤트 루프:
[타이머] → [I/O 콜백] → [Poll] → [Check] → [Close]
↑ 여기서 대부분의 I/O 처리
비동기 I/O (DB, 파일, 네트워크) → 이벤트 루프 블로킹 없음 ✅
동기 CPU 작업 (암호화, 이미지 처리, 복잡한 계산) → 블로킹 ❌
// 나쁜 예: Event Loop 블로킹
app.get('/hash', (req, res) => {
// 동기 암호화 — 처리 중 다른 요청 모두 대기
const hash = crypto.scryptSync(password, salt, 64);
res.json({ hash: hash.toString('hex') });
});
// 좋은 예: 비동기 처리
app.get('/hash', async (req, res) => {
const hash = await new Promise((resolve, reject) => {
crypto.scrypt(password, salt, 64, (err, key) => {
if (err) reject(err);
else resolve(key);
});
});
res.json({ hash: hash.toString('hex') });
});
// Event Loop 지연 측정
const { monitorEventLoopDelay } = require('perf_hooks');
const histogram = monitorEventLoopDelay({ resolution: 20 });
histogram.enable();
setInterval(() => {
const delay = histogram.mean / 1e6; // ns → ms
if (delay > 10) {
console.warn(`Event Loop 지연: ${delay.toFixed(2)}ms`);
}
histogram.reset();
}, 5000);
// clinic.js로 Event Loop 병목 분석
// npm install -g clinic
// clinic doctor -- node app.js
// 결과 HTML 자동 생성

Node.js는 기본적으로 단일 코어만 사용합니다. cluster 모듈로 멀티코어를 활용하세요.

cluster.js
const cluster = require('cluster');
const os = require('os');
if (cluster.isPrimary) {
const numCPUs = os.cpus().length;
console.log(`Primary ${process.pid}: ${numCPUs} workers 시작`);
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} 종료 → 재시작`);
cluster.fork(); // 자동 재시작
});
} else {
require('./app.js'); // 각 worker에서 앱 실행
console.log(`Worker ${process.pid} 시작`);
}
Terminal window
# PM2로 더 쉽게 (권장)
npm install -g pm2
# CPU 코어 수만큼 인스턴스 실행
pm2 start app.js -i max
# 상태 확인
pm2 status
pm2 monit
// Worker Threads로 CPU 집중 작업 분리
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');
if (isMainThread) {
// 메인 스레드 — HTTP 요청 처리
app.post('/process-image', async (req, res) => {
const result = await runWorker(req.body.imageBuffer);
res.json(result);
});
function runWorker(data) {
return new Promise((resolve, reject) => {
const worker = new Worker(__filename, { workerData: data });
worker.on('message', resolve);
worker.on('error', reject);
});
}
} else {
// Worker 스레드 — 이미지 처리 (Event Loop 영향 없음)
const result = processImage(workerData);
parentPort.postMessage(result);
}
// 스트림으로 대용량 파일 처리
const fs = require('fs');
const readline = require('readline');
// 나쁜 예: 전체 파일 메모리 로드
const data = fs.readFileSync('large-file.csv'); // 1GB 파일이면 1GB 메모리
// 좋은 예: 스트림 처리
const rl = readline.createInterface({
input: fs.createReadStream('large-file.csv'),
crlfDelay: Infinity,
});
rl.on('line', (line) => {
processLine(line); // 한 줄씩 처리
});
// 메모리 사용량 모니터링
setInterval(() => {
const mem = process.memoryUsage();
console.log({
rss: `${(mem.rss / 1024 / 1024).toFixed(2)} MB`, // 전체 메모리
heapUsed: `${(mem.heapUsed / 1024 / 1024).toFixed(2)} MB`, // 힙 사용
heapTotal: `${(mem.heapTotal / 1024 / 1024).toFixed(2)} MB`, // 힙 전체
});
}, 10000);
// Express gzip 압축
const compression = require('compression');
app.use(compression({
level: 6, // 압축 레벨 (1-9)
threshold: 1024, // 1KB 이상만 압축
filter: (req, res) => {
if (req.headers['x-no-compression']) return false;
return compression.filter(req, res);
},
}));
Terminal window
# Node.js 내장 프로파일러
node --prof app.js
# 부하 실행 후 Ctrl+C
# 프로파일 결과 분석
node --prof-process isolate-*.log > profile.txt
visitor count

이 가이드를 내 서비스에 직접 적용해 보세요.

TestForge 무료 스캔 시작 →