Node.js 워커 스레드로 CPU 집약 작업 병렬 처리하기

문제 상황

이미지 처리 API 서버에서 동시에 여러 리사이징 요청이 들어오면 응답 시간이 급격히 증가하는 현상이 발생했다. Sharp 라이브러리를 사용한 이미지 처리가 CPU를 많이 사용하면서 메인 스레드를 블로킹했고, 다른 요청들까지 대기하게 되었다.

Worker Threads 도입

Node.js의 worker_threads 모듈을 사용해 이미지 처리를 별도 스레드로 분리했다.

const { Worker } = require('worker_threads');
const os = require('os');

class WorkerPool {
  constructor(workerPath, poolSize = os.cpus().length) {
    this.workerPath = workerPath;
    this.workers = [];
    this.queue = [];
    
    for (let i = 0; i < poolSize; i++) {
      this.workers.push(this.createWorker());
    }
  }

  createWorker() {
    const worker = new Worker(this.workerPath);
    worker.isAvailable = true;
    
    worker.on('message', (result) => {
      worker.isAvailable = true;
      worker.currentResolve(result);
      this.processQueue();
    });
    
    return worker;
  }

  async execute(data) {
    return new Promise((resolve, reject) => {
      this.queue.push({ data, resolve, reject });
      this.processQueue();
    });
  }

  processQueue() {
    if (this.queue.length === 0) return;
    
    const availableWorker = this.workers.find(w => w.isAvailable);
    if (!availableWorker) return;
    
    const { data, resolve } = this.queue.shift();
    availableWorker.isAvailable = false;
    availableWorker.currentResolve = resolve;
    availableWorker.postMessage(data);
  }
}

워커 파일은 다음과 같이 구현했다.

// image-worker.js
const { parentPort } = require('worker_threads');
const sharp = require('sharp');

parentPort.on('message', async ({ buffer, width, height }) => {
  try {
    const processed = await sharp(buffer)
      .resize(width, height)
      .toBuffer();
    
    parentPort.postMessage({ success: true, data: processed });
  } catch (error) {
    parentPort.postMessage({ success: false, error: error.message });
  }
});

결과

  • 동시 요청 10개 기준 평균 응답 시간: 3.2초 → 0.9초
  • CPU 코어를 효율적으로 활용하며 처리량 4배 증가
  • 메인 스레드는 요청 라우팅에만 집중 가능

주의사항

워커 간 데이터 전달 시 직렬화 비용이 발생한다. 큰 객체보다는 Buffer나 SharedArrayBuffer 사용을 고려해야 한다. 또한 워커 풀 크기는 CPU 코어 수에 맞춰 조정하되, 메모리 사용량도 함께 모니터링해야 한다.