Node.js 성능 최적화 — Event Loop 병목부터 클러스터까지
Node.js는 단일 스레드 이벤트 루프 구조이기 때문에 CPU 집중 작업이 병목이 되면 모든 요청이 멈춥니다. 올바른 최적화 방법을 알아봅니다.
Event Loop 이해
섹션 제목: “Event Loop 이해”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 지연 측정
섹션 제목: “Event Loop 지연 측정”// 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 자동 생성Cluster 모드 — CPU 코어 활용
섹션 제목: “Cluster 모드 — CPU 코어 활용”Node.js는 기본적으로 단일 코어만 사용합니다.
cluster 모듈로 멀티코어를 활용하세요.
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} 시작`);}# PM2로 더 쉽게 (권장)npm install -g pm2
# CPU 코어 수만큼 인스턴스 실행pm2 start app.js -i max
# 상태 확인pm2 statuspm2 monitCPU 집중 작업 오프로드
섹션 제목: “CPU 집중 작업 오프로드”// 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);응답 압축 & HTTP/2
섹션 제목: “응답 압축 & HTTP/2”// 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); },}));프로파일링
섹션 제목: “프로파일링”# Node.js 내장 프로파일러node --prof app.js# 부하 실행 후 Ctrl+C
# 프로파일 결과 분석node --prof-process isolate-*.log > profile.txt다음 단계
섹션 제목: “다음 단계”이 가이드를 내 서비스에 직접 적용해 보세요.
TestForge 무료 스캔 시작 →