3 min read

Node.js Worker Threads로 CPU 집약적 작업 처리하기

Node.js의 싱글 스레드 한계를 극복하는 Worker Threads 활용법을 실무 예제와 함께 알아봅니다.

Node.jsPerformance

Node.js는 이벤트 루프 기반의 싱글 스레드 모델로 유명합니다. I/O 작업에는 뛰어난 성능을 보이지만, CPU 집약적인 작업에서는 한계가 있죠.

왜 Worker Threads가 필요한가?

// ❌ 이 코드는 이벤트 루프를 블로킹합니다
app.get('/heavy-computation', (req, res) => {
  const result = fibonacci(45); // 몇 초간 서버 전체가 멈춤
  res.json({ result });
});
 
// 다른 모든 요청이 대기하게 됨...

Worker Threads 기본 사용법

메인 스레드 (main.js)

const { Worker, isMainThread, parentPort } = require('worker_threads');
 
if (isMainThread) {
  const worker = new Worker('./worker.js');
  
  worker.postMessage({ num: 45 });
  
  worker.on('message', (result) => {
    console.log('결과:', result);
  });
  
  worker.on('error', (err) => {
    console.error('Worker 에러:', err);
  });
}

워커 스레드 (worker.js)

const { parentPort } = require('worker_threads');
 
function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}
 
parentPort.on('message', (data) => {
  const result = fibonacci(data.num);
  parentPort.postMessage(result);
});

실무 패턴: Worker Pool

매번 Worker를 생성하는 것은 비효율적입니다. Worker Pool을 만들어 재사용하세요:

import { Worker } from 'worker_threads';
import { cpus } from 'os';
 
class WorkerPool {
  private workers: Worker[] = [];
  private queue: Array<{
    task: any;
    resolve: (value: any) => void;
    reject: (error: Error) => void;
  }> = [];
  private freeWorkers: Worker[] = [];
 
  constructor(
    private workerPath: string,
    private poolSize: number = cpus().length
  ) {
    this.init();
  }
 
  async execute<T>(task: any): Promise<T> {
    return new Promise((resolve, reject) => {
      this.queue.push({ task, resolve, reject });
      this.processQueue();
    });
  }
 
  private processQueue() {
    if (this.queue.length === 0 || this.freeWorkers.length === 0) {
      return;
    }
 
    const worker = this.freeWorkers.pop()!;
    const { task, resolve } = this.queue.shift()!;
 
    worker.once('message', resolve);
    worker.postMessage(task);
  }
}

언제 Worker Threads를 사용해야 할까?

✅ 적합한 경우

  • 이미지/비디오 처리
  • 암호화/해시 연산
  • 복잡한 수학 계산
  • 대용량 데이터 파싱

❌ 부적합한 경우

  • 단순 I/O 작업 (이미 비동기로 처리됨)
  • 가벼운 연산 (Worker 생성 오버헤드가 더 큼)

성능 비교

// 벤치마크: fibonacci(40) 계산
// 싱글 스레드: 1,200ms (서버 블로킹)
// Worker 1개: 1,250ms (메인 스레드 자유)
// Worker 4개 병렬: 320ms

결론

Worker Threads는 Node.js의 약점이었던 CPU 집약적 작업을 효과적으로 처리할 수 있게 해줍니다. 작업의 특성을 파악하고 적절히 활용하세요.