프로세스, 스레드, 메모리 — Node.js를 이해하기 위한 OS 기초 (2편)

정기창·

1편에서 Node.js가 V8 엔진과 libuv로 구성된 런타임이라는 것을 확인했습니다. libuv는 내부적으로 스레드 풀이라는 것을 사용해서 파일 I/O 같은 작업을 처리한다고 했는데, 이 스레드 풀이 무엇인지 제대로 이해하려면 먼저 OS 수준에서 몇 가지 개념을 짚고 넘어가야 합니다.

프로세스가 뭔지, 스레드가 뭔지, 메모리는 어떻게 동작하는지. 이런 기초 없이 "libuv가 스레드 풀 4개로 파일을 읽는다"는 설명을 들어도 와닿지 않았습니다. 그래서 이번 글에서는 Node.js 코드에서 한 발짝 물러나, 컴퓨터가 프로그램을 어떻게 실행하는지부터 정리해보겠습니다.

프로그램은 어디에 있고, 어떻게 실행되는가

컴퓨터에서 프로그램이 실행되기까지의 흐름은 세 가지 하드웨어를 거칩니다.

구성 요소역할특징
SSD/HDD (저장소)프로그램 파일 보관전원 꺼져도 유지, 느림
RAM (메모리)실행 중인 프로그램 상주전원 끄면 사라짐, 빠름
CPU (프로세서)연산, 명령 처리가장 빠름

프로그램을 실행하면 이런 흐름이 됩니다.

SSD/HDD에 저장된 프로그램 파일
    ↓ 메모리로 로딩
RAM에 프로세스로 상주
    ↓ 명령어 실행
CPU가 연산 처리

VS Code를 더블클릭하면, SSD에 있는 VS Code 파일이 메모리에 올라가면서 프로세스가 됩니다. CPU는 그 프로세스의 명령어를 하나씩 읽어서 실행합니다. node app.js를 터미널에 입력하는 것도 마찬가지입니다. SSD에 설치된 Node.js 바이너리가 메모리에 올라가고, app.js 파일을 읽어서 V8 엔진이 실행합니다.

SSD와 HDD

SSD와 HDD 모두 전원이 꺼져도 데이터가 유지되는 저장소라는 점에서 역할은 같습니다. 차이는 속도와 물리적 구조에 있습니다.

항목HDDSSD
방식자기 디스크를 물리적으로 회전반도체 칩에 전기적 읽기/쓰기
속도느림빠름
내구성충격에 약함충격에 강함

SSD가 빠르면 프로그램 시작 속도가 빨라지고, Node.js에서 fs.readFile() 같은 파일 I/O 작업의 저장소 → 메모리 구간도 빨라집니다. 요즘 대부분의 컴퓨터가 SSD를 사용하는 이유이기도 합니다.

프로세스와 스레드

방금 "프로그램이 메모리에 올라가면 프로세스가 된다"고 했습니다. 이 프로세스 안에서 실제로 일을 하는 단위가 스레드입니다.

프로세스 — 실행 중인 프로그램

프로세스는 OS가 프로그램에 독립된 메모리 공간을 할당해서 만든 실행 단위입니다. Chrome을 켜면 Chrome 프로세스가 생기고, VS Code를 켜면 VS Code 프로세스가 생깁니다. 각 프로세스는 자기만의 메모리를 가지고 있어서, Chrome이 죽어도 VS Code에 영향을 주지 않습니다.

스레드 — 프로세스 안의 실행 단위

하나의 프로세스 안에서 여러 작업을 동시에 하고 싶으면 스레드를 만듭니다. 같은 프로세스의 스레드들은 메모리를 공유하면서 각자 다른 일을 할 수 있습니다.

프로세스 (Chrome)
├─ 스레드 1: 탭 1 렌더링
├─ 스레드 2: 탭 2 렌더링
├─ 스레드 3: 네트워크 요청
└─ 스레드 4: 파일 다운로드
     ↑ 같은 메모리를 공유하면서 동시에 일함
구분프로세스스레드
메모리독립 (각자 별도)공유 (같은 프로세스 내)
생성 비용큼작음
하나가 죽으면다른 프로세스에 영향 없음같은 프로세스의 다른 스레드에 영향
예시Chrome, VS Code 각각Chrome 안의 탭, 네트워크 처리

Node.js에서 "싱글 스레드"라고 할 때, 이 스레드가 바로 여기서 말하는 스레드입니다. JavaScript 코드를 실행하는 스레드가 하나라는 뜻입니다. 하지만 libuv의 스레드 풀은 별도의 스레드를 가지고 있으므로, Node.js 프로세스 전체가 스레드 하나로만 돌아가는 것은 아닙니다.

CPU 코어와 스레드의 관계

프로그램에서 스레드를 여러 개 만든다고 했는데, 그 스레드들은 누가 실행하는 걸까요? CPU 코어입니다.

하드웨어 스레드 vs 소프트웨어 스레드

"스레드"라는 단어가 두 가지 맥락에서 쓰이기 때문에 혼란이 생길 수 있습니다.

구분의미예시
하드웨어 스레드CPU 코어가 동시에 처리할 수 있는 수M2 Pro: 12코어 12스레드
소프트웨어 스레드코드에서 만드는 실행 단위수백~수천 개 가능

Intel CPU에서 "8코어 16스레드"라는 스펙을 본 적 있을 것입니다. 이건 하이퍼스레딩 기술로 코어 1개가 스레드 2개를 처리하는 것입니다. Apple Silicon(M1, M2 등)은 하이퍼스레딩이 없어서 코어 수와 하드웨어 스레드 수가 같습니다.

CPU 코어가 12개인데 소프트웨어 스레드가 100개면 어떻게 될까요? OS가 컨텍스트 스위칭이라는 방법으로 스레드들을 빠르게 번갈아 실행합니다. 한 번에 12개만 진짜 동시에 실행되지만, 나머지 스레드도 아주 빠르게 교대하기 때문에 사람 눈에는 모두 동시에 돌아가는 것처럼 보입니다.

스레드 풀이 뭔가

"풀(Pool)"이라는 단어가 붙으면 미리 만들어놓고 재사용한다는 의미입니다. 커넥션 풀, 스레드 풀 모두 같은 패턴입니다.

풀 없이: 요청 → 스레드 생성 → 작업 → 스레드 파괴 (매번 비용 발생)
풀 방식: Node.js 시작 → 스레드 4개 미리 생성 → 작업 할당 → 반납 → 재사용

libuv는 Node.js가 시작될 때 기본 4개의 워커 스레드를 미리 만들어둡니다. 파일 I/O 같은 작업이 들어오면 빈 워커에 할당하고, 작업이 끝나면 워커를 다시 풀로 반납합니다. UV_THREADPOOL_SIZE 환경변수로 최대 1024개까지 늘릴 수 있습니다.

4개 워커의 의미

워커가 4개라는 것은 동시에 4개의 작업을 병렬로 처리할 수 있다는 뜻입니다.

fs.readFile('a.txt')   → 워커 1이 처리
fs.readFile('b.txt')   → 워커 2가 처리
fs.readFile('c.txt')   → 워커 3이 처리
fs.readFile('d.txt')   → 워커 4가 처리
fs.readFile('e.txt')   → 대기열에서 워커가 비길 기다림

5번째 파일 읽기 요청이 오면 4개 워커가 모두 바쁘기 때문에 큐에서 대기합니다. 기존 워커 중 하나가 작업을 마치고 반납되면 그때 5번째 작업이 시작됩니다.

작업 큐는 어떻게 동작하는가

여기서 "큐에서 대기한다"는 말이 좀 더 궁금해질 수 있습니다. 이 큐는 누가 관리하는 걸까요?

작업 큐는 스레드가 아닙니다. 프로세스 메모리에 올라가 있는 데이터 구조(자료구조)일 뿐입니다. "할 일 목록"이 적혀있는 메모장이라고 생각하면 됩니다. 큐 자체가 무언가를 실행하지 않습니다.

Node.js 프로세스 (메모리)
├─ 메인 스레드 (V8, JS 실행)        ← 실행 단위
├─ 워커 스레드 1~4                   ← 실행 단위
└─ 작업 큐 (wq)                     ← 데이터 (할 일 목록)

큐에 넣고 빼는 역할도 나뉘어 있습니다.

  • 큐에 넣는 것 → 메인 스레드가 합니다. fs.readFile()을 호출하면 메인 스레드가 작업을 큐에 추가합니다.
  • 큐에서 꺼내는 것 → 워커 스레드가 합니다. 빈 워커가 큐에서 작업을 가져갑니다.
메인 스레드: fs.readFile() 호출 → 큐에 작업 추가
                                     ↓
워커 스레드: 큐에서 작업 꺼냄 → 실행 → 완료 → 다시 큐 확인

이 구조를 프로듀서-컨슈머(Producer-Consumer) 패턴이라고 부릅니다. 메인 스레드가 일감을 만들어서 큐에 넣고(produce), 워커 스레드가 큐에서 꺼내서 처리(consume)합니다. 사실 1~4번째 작업도 큐를 거칩니다. 다만 빈 워커가 있으니 즉시 꺼내가는 것뿐이고, 5번째처럼 빈 워커가 없을 때만 큐에 실제로 대기하게 됩니다.

할 일 없는 워커는?

할 일이 없는 워커 스레드는 sleep 상태에 들어갑니다. CPU를 점유하지 않습니다. OS가 "이 스레드는 지금 할 일 없음"이라고 판단하면 CPU 코어 스케줄링에서 제외시킵니다. 그래서 Node.js가 켜져있다고 해서 libuv 스레드 풀이 CPU 코어 4개를 영구적으로 잡고 있는 건 아닙니다.

CPU 코어스레드 풀 4개실제 동작
1코어4개 스레드번갈아 실행 (동시처럼 보임)
4코어4개 스레드진짜 동시 실행
12코어 (M2 Pro)4개 스레드진짜 동시 실행, 나머지 코어는 V8·OS 등

프로세스의 메모리 관리

프로세스가 시작될 때 필요한 메모리를 한꺼번에 다 받는 것은 아닙니다. 필요할 때마다 OS에 요청해서 동적으로 할당받습니다.

Node.js에서는 V8 엔진이 메모리를 관리합니다. V8의 힙(Heap) 메모리 기본 한계는 약 1.5GB(64bit 기준)이고, --max-old-space-size 옵션으로 조절할 수 있습니다. 이 한계를 넘으면 JavaScript heap out of memory 에러가 발생하면서 프로세스가 종료됩니다.

# V8 힙 메모리를 4GB로 늘리기
node --max-old-space-size=4096 app.js

메모리가 부족해지면 V8의 가비지 컬렉터(GC)가 더 이상 사용하지 않는 객체를 찾아서 메모리를 해제합니다. 이 과정에 대해서는 이 시리즈 후반부에서 다루겠습니다.

이것들이 Node.js와 어떻게 연결되는가

이번 글에서 다룬 개념들을 Node.js 관점으로 정리하면 이렇게 됩니다.

OS 개념Node.js에서의 의미
프로세스node app.js를 실행하면 생기는 Node.js 프로세스
메인 스레드V8이 JavaScript를 실행하는 싱글 스레드
스레드 풀libuv가 파일 I/O 등을 처리하는 워커 스레드 (기본 4개)
작업 큐워커가 바쁠 때 작업이 대기하는 메모리 상의 자료구조
메모리V8 힙 (~1.5GB 기본), 동적 할당
CPU 코어메인 스레드 + 워커 스레드들이 OS 스케줄링에 의해 배치됨

Node.js가 "싱글 스레드"라는 말은 JavaScript 실행 스레드가 하나라는 뜻이지, Node.js 프로세스 전체가 하나의 스레드로 동작한다는 뜻이 아닙니다. libuv 스레드 풀, V8 GC 스레드 등 내부적으로는 여러 스레드가 함께 일하고 있습니다.

다음 글(3편)에서는 이 싱글 스레드 위에서 어떻게 여러 작업을 동시에 처리하는 것처럼 보이는지, 콜 스택과 이벤트 루프를 통해 살펴보겠습니다.

Node.js프로세스스레드OSCPU메모리스레드 풀면접 준비