DevToolBox무료
블로그

Node.js 성능 최적화: 클러스터링, 스트림, 프로파일링

12분by DevToolBox

Node.js는 단일 서버에서 수만 건의 동시 연결을 처리할 수 있습니다. 이 가이드는 클러스터링, 스트림, 프로파일링, 캐싱 전략을 다룹니다.

이벤트 루프 이해

Node.js는 단일 스레드입니다. 이벤트 루프를 차단하면 모든 요청이 차단됩니다.

// NEVER do this — blocks the event loop
app.get('/compute', (req, res) => {
  // Synchronous CPU-heavy computation blocks ALL requests
  let result = 0;
  for (let i = 0; i < 1e9; i++) result += i;  // 1 billion iterations!
  res.json({ result });
});

// DO THIS instead — offload to worker thread
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');

app.get('/compute', (req, res) => {
  const worker = new Worker('./computeWorker.js', {
    workerData: { input: req.query.n }
  });
  worker.on('message', result => res.json({ result }));
  worker.on('error', err => res.status(500).json({ error: err.message }));
});

멀티코어 성능을 위한 클러스터링

Node.js는 기본적으로 단일 CPU 코어에서 실행됩니다.

// Node.js Cluster Module — Use All CPU Cores
const cluster = require('cluster');
const os = require('os');
const express = require('express');

const NUM_WORKERS = os.cpus().length;

if (cluster.isPrimary) {
  console.log(`Primary ${process.pid} is running`);
  console.log(`Starting ${NUM_WORKERS} workers...`);

  // Fork workers
  for (let i = 0; i < NUM_WORKERS; i++) {
    cluster.fork();
  }

  cluster.on('exit', (worker, code, signal) => {
    console.log(`Worker ${worker.process.pid} died (${signal || code}). Restarting...`);
    cluster.fork(); // Auto-restart crashed workers
  });

  cluster.on('online', (worker) => {
    console.log(`Worker ${worker.process.pid} is online`);
  });

} else {
  // Worker process — runs the actual server
  const app = express();

  app.get('/api/users', async (req, res) => {
    const users = await db.getUsers();
    res.json(users);
  });

  app.listen(3000, () => {
    console.log(`Worker ${process.pid} listening on port 3000`);
  });
}

// Alternative: PM2 cluster mode (recommended for production)
// pm2 start server.js -i max   # auto-detect CPU count
// pm2 start server.js -i 4     # explicit count

메모리 효율을 위한 스트림

스트림은 모든 것을 메모리에 로드하지 않고 데이터를 조각별로 처리합니다.

// Node.js Streams — Memory-Efficient Processing

const fs = require('fs');
const { Transform, pipeline } = require('stream');
const { promisify } = require('util');
const pipelineAsync = promisify(pipeline);

// 1. Stream a large file as HTTP response (no memory buffering)
app.get('/download/large-file', (req, res) => {
  const filePath = './large-file.csv';
  const stat = fs.statSync(filePath);

  res.setHeader('Content-Type', 'text/csv');
  res.setHeader('Content-Length', stat.size);
  res.setHeader('Content-Disposition', 'attachment; filename=data.csv');

  // Pipe file directly to response — never fully in memory
  fs.createReadStream(filePath).pipe(res);
});

// 2. Transform stream for CSV processing
class CsvParser extends Transform {
  constructor() {
    super({ objectMode: true });
    this.buffer = '';
    this.headers = null;
  }

  _transform(chunk, encoding, callback) {
    this.buffer += chunk.toString();
    const lines = this.buffer.split('\n');
    this.buffer = lines.pop(); // Keep incomplete line in buffer

    for (const line of lines) {
      if (!this.headers) {
        this.headers = line.split(',');
        continue;
      }
      const values = line.split(',');
      const record = {};
      this.headers.forEach((h, i) => record[h.trim()] = values[i]?.trim());
      this.push(record);
    }
    callback();
  }
}

// 3. Pipeline for reliable error handling
async function processLargeCsvFile(inputPath, outputPath) {
  await pipelineAsync(
    fs.createReadStream(inputPath),
    new CsvParser(),
    new Transform({
      objectMode: true,
      transform(record, enc, cb) {
        // Transform each record
        record.processed = true;
        cb(null, JSON.stringify(record) + '\n');
      }
    }),
    fs.createWriteStream(outputPath)
  );
  console.log('Processing complete');
}

캐싱 전략

캐싱은 가장 영향력 있는 성능 최적화입니다.

// Caching Strategies for Node.js

// 1. In-Memory LRU Cache
const { LRUCache } = require('lru-cache');

const cache = new LRUCache({
  max: 500,           // Maximum 500 items
  ttl: 5 * 60 * 1000, // 5 minutes TTL
  allowStale: true,   // Return stale value while refreshing
  updateAgeOnGet: true,
});

async function getUser(id) {
  const cacheKey = `user:${id}`;
  const cached = cache.get(cacheKey);
  if (cached) return cached;

  const user = await db.findUser(id);
  cache.set(cacheKey, user);
  return user;
}

// 2. Redis Cache with Stale-While-Revalidate
const Redis = require('ioredis');
const redis = new Redis();

async function getCachedData(key, fetchFn, ttl = 300) {
  const [cached, ttlRemaining] = await redis.pipeline()
    .get(key)
    .ttl(key)
    .exec();

  if (cached[1]) {
    const data = JSON.parse(cached[1]);

    // Background refresh when < 60 seconds remaining
    if (ttlRemaining[1] < 60) {
      fetchFn().then(fresh =>
        redis.setex(key, ttl, JSON.stringify(fresh))
      );
    }

    return data;
  }

  const data = await fetchFn();
  await redis.setex(key, ttl, JSON.stringify(data));
  return data;
}

// 3. HTTP Response Caching with ETags
app.get('/api/products', async (req, res) => {
  const products = await getProducts();
  const etag = require('crypto')
    .createHash('md5')
    .update(JSON.stringify(products))
    .digest('hex');

  if (req.headers['if-none-match'] === etag) {
    return res.status(304).end();
  }

  res.setHeader('ETag', etag);
  res.setHeader('Cache-Control', 'public, max-age=60, stale-while-revalidate=300');
  res.json(products);
});

자주 묻는 질문

클러스터 워커는 얼마나 만들어야 하나요?

CPU 코어당 하나의 워커를 생성하세요, os.cpus().length개의 워커.

스트림 vs 메모리 로드 중 언제 사용해야 하나요?

10MB 이상의 파일 처리, 데이터 파이핑, 점진적 처리에 스트림을 사용하세요.

--inspect 플래그란?

--inspect 플래그는 V8 인스펙터 프로토콜이 활성화된 상태로 Node.js를 시작합니다.

왜 Node.js 앱이 메모리를 많이 사용하나요?

일반적인 원인: 메모리 누수, 제거 정책 없는 캐시, 대용량 인메모리 데이터셋.

관련 도구

𝕏 Twitterin LinkedIn
도움이 되었나요?

최신 소식 받기

주간 개발 팁과 새 도구 알림을 받으세요.

스팸 없음. 언제든 구독 해지 가능.

Try These Related Tools

{ }JSON Formatter

Related Articles

Node.js Streams 완전 가이드: Readable, Writable, Transform & Pipeline

Node.js 스트림 마스터 — Readable, Writable, Transform, Pipeline API.

Redis 캐싱 패턴: 웹 애플리케이션 완전 가이드 (2026)

Redis 캐싱 패턴을 배우세요.

Docker 모범 사례: 프로덕션 컨테이너를 위한 20가지 팁

Docker 필수 모범 사례 20가지: 멀티 스테이지 빌드, 보안 강화, 이미지 최적화, 캐시 전략, CI/CD 자동화.