Node.js는 싱글 쓰레드인데, 멀티 스레드를 사용할 수 있다?(feat. libuv , Worker Threads)

소일로·2024년 11월 21일
0
post-thumbnail

글을 쓰게 된 동기

Node.js 가 어떤식으로 외부 I/O 작업이 동작하는지 어렴풋이 알고 있었는데 글을 쓰면서 정리를 하고 싶어 글을 작성하게 되었습니다. Node.js는 단일 쓰레드로 동작하지만, 효율적인 비동기 I/O 처리를 통해 대규모 동시성을 지원하는 서버 환경을 제공합니다. 본 글에서는 Node.js의 단일 쓰레드 모델이 어떻게 다양한 I/O 작업(DB, 파일, 네트워크 등)을 처리하며, 하드웨어 쓰레드의 활용을 통해 성능을 극대화하는지 구체적으로 알아보겠습니다.

1. Node.js의 단일 쓰레드 모델

1.1. 단일 쓰레드란?

Node.js의 이벤트 루프는 단일 쓰레드에서 실행됩니다. 이는 코드 실행, 이벤트 처리, 콜백 함수 호출 등이 하나의 쓰레드에서 순차적으로 이루어진다는 것을 의미합니다.

  • 장점:
    • 쓰레드 생성 및 관리에 대한 오버헤드가 적음.
    • 멀티쓰레드 환경에서 발생하는 Race ConditionDeadlock 같은 문제를 방지.
  • 제한:
    • CPU 연산이 많은 작업(예: 대규모 데이터 처리)이 단일 쓰레드에 부하를 주어 응답 속도가 느려질 수 있음.

2. 비동기 I/O와 단일 쓰레드의 관계

Node.js는 단일 쓰레드에서 비동기 I/O를 활용하여 효율적인 처리를 수행합니다. 이를 가능하게 하는 핵심은 이벤트 루프(Event Loop)백그라운드 스레드(pool)입니다.

2.1. 이벤트 루프(Event Loop)

이벤트 루프는 Node.js의 핵심으로, 다음과 같은 과정을 통해 비동기 작업을 처리합니다:

  1. I/O 작업 요청:
    • 파일 읽기/쓰기, 네트워크 요청, DB 쿼리 등은 이벤트 루프를 통해 처리 대기열로 전달됩니다.
  2. 백그라운드 처리:
    • I/O 작업은 Node.js의 내부 스레드 풀(libuv)이나 OS의 네이티브 I/O 인터페이스를 통해 비동기로 처리됩니다.
  3. 콜백 실행:
    • I/O 작업이 완료되면, 결과가 이벤트 루프에 반환되고 콜백 함수가 실행됩니다.

이 과정에서 Node.js의 메인 쓰레드는 계속 이벤트 루프를 돌며 새로운 요청을 처리합니다.


3. Node.js의 I/O 작업 처리 방식

3.1. 데이터베이스 I/O

  • 데이터베이스 쿼리는 네트워크 I/O 작업으로 처리됩니다.
  • Node.js는 단일 쓰레드에서 쿼리를 실행하지 않고, 백그라운드 스레드 또는 DB 드라이버의 네이티브 코드를 통해 비동기로 처리합니다.
  • 예: MySQL, PostgreSQL 등의 Node.js 드라이버는 내부적으로 네이티브 C/C++ 모듈을 활용해 DB 서버와 통신합니다.
javascript
const mysql = require('mysql2/promise');

(async () => {
  const connection = await mysql.createConnection({host: 'localhost', user: 'root', database: 'test'});

// DB 쿼리 비동기 처리const [rows] = await connection.execute('SELECT * FROM users');
  console.log(rows);// 쿼리 결과는 이벤트 루프가 아닌 백그라운드에서 처리
})();

3.2. 파일 I/O

Node.js의 fs 모듈은 파일 읽기/쓰기를 비동기로 처리합니다. 이는 OS의 비동기 파일 API를 활용하거나, libuv 스레드 풀을 통해 동작합니다.

javascript
const fs = require('fs');

fs.readFile('example.txt', 'utf8', (err, data) => {
  if (err) throw err;
  console.log(data);// 파일 읽기는 libuv의 스레드 풀에서 처리
});

3.3. 네트워크 I/O

Node.js의 네트워크 요청은 OS 커널의 네이티브 I/O 인터페이스를 활용해 처리됩니다. 이를 통해 효율적으로 다수의 요청을 동시에 처리할 수 있습니다.


4. 하드웨어 스레드의 활용

Node.js는 단일 쓰레드로 동작하지만, libuv를 통해 하드웨어 스레드를 효과적으로 활용합니다. libuv는 Node.js에서 비동기 I/O 작업을 처리하는 C 라이브러리로, 다음과 같은 역할을 합니다:

4.1. libuv 스레드 풀

  • 일부 CPU 연산이나 블로킹 I/O 작업은 libuv의 스레드 풀에서 처리됩니다.
  • 기본적으로 4개의 스레드가 할당되며, 필요에 따라 환경 변수를 통해 조정할 수 있습니다.
    • 설정 방법: UV_THREADPOOL_SIZE=8

4.2. OS 네이티브 I/O

  • 파일, 네트워크 같은 I/O 작업은 OS의 비동기 API(예: Linux의 epoll, Windows의 IOCP)를 사용하여 처리됩니다.
  • 네이티브 I/O 인터페이스를 활용하면, Node.js의 단일 쓰레드가 부하 없이 여러 작업을 동시에 처리할 수 있습니다.

5. 단일 쓰레드와 멀티 쓰레드 비교

5.1. 단일 쓰레드의 특징

  • 장점:
    • 간단한 구조와 코드 작성.
    • 동기화 문제(Race Condition, Deadlock) 없음.
  • 단점:
    • CPU 연산이 많은 작업에서 성능 저하 발생.

5.2. 멀티 쓰레드의 특징

  • 장점:
    • 여러 쓰레드를 사용하여 병렬로 작업 처리.
    • CPU를 효율적으로 활용 가능.
  • 단점:
    • 쓰레드 관리 오버헤드.
    • 동기화 문제가 발생할 가능성.

Node.js는 이러한 단점을 보완하기 위해 단일 쓰레드를 유지하면서, libuv 및 OS의 기능을 통해 멀티 쓰레드의 이점을 활용합니다.


6. 단일 쓰레드의 한계와 해결 방법

6.1. CPU 집중 작업

Node.js는 기본적으로 CPU 집약적인 작업에는 적합하지 않습니다. 이런 경우 워커 스레드(Worker Threads)를 활용할 수 있습니다.

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

const worker = new Worker('./worker.js');
worker.on('message', (result) => {
  console.log('Result:', result);// 워커 스레드에서 결과 수신
});

6.2. 클러스터링(Clustering)

Node.js는 클러스터링을 통해 단일 프로세스를 여러 CPU 코어에 분산할 수 있습니다.

javascript
const cluster = require('cluster');

if (cluster.isMaster) {
  for (let i = 0; i < require('os').cpus().length; i++) {
    cluster.fork();
  }
} else {
  require('./server.js');// 각 프로세스가 서버 역할 수행
}

7. 결론

Node.js의 단일 쓰레드는 단순하고 효율적인 이벤트 기반 모델로, 대규모 동시성을 지원하는데 탁월한 성능을 발휘합니다. 비록 단일 쓰레드로 동작하지만, libuv를 통해 하드웨어 스레드를 활용하여 다양한 I/O 작업을 처리함으로써 멀티 쓰레드의 이점을 효율적으로 사용합니다.

Node.js의 구조와 처리 방식을 이해하면, 특정 상황에 적합한 설계를 통해 최대의 성능을 발휘할 수 있습니다.

profile
백엔드 개발자

0개의 댓글