제목 : 프로세스와 스레드
위의 주제로 글을 작성하게 된 이유
프로세스와 스레드는 우리가 사용하는 프로그래밍 언어마다 각각 프로세스와 스레드를 사용하는 방식이 다르다.
심지어는 이 두 개의 리소스를 어떤 인프라 환경에서 사용하게 되냐에 따라서도 달라지게 되는데,
요즘은 Docker 와 k8s(kubernetes) 라는 편리한 컨테이너, 네임스페이스 관리 프로그램이 존재하여
우리가 크게 프로세스와 스레드에 대해 신경 쓸 필요는 없었다.
나는 C 언어를 대학교에서 배우고, Java 를 배우게 되었으며, 그 이후 Swift(찍먹),
JavaScript, TypeScript (JS 수퍼셋) 을 배우게 되었다.
다양한 언어를 접했지만, 이 프로그래밍 언어들이 기기의 환경에 알맞게 실행되게 컴파일 된다는 것 까지만 알았고,
GC (Garbage Collection), JVM(Java Virtual Machine) 과 같은 어려운 요소는 배우려 하지 않았었다.
그러나, 나는 프로그래머스 풀스택 부트캠프에서 JavaScript, TypeScript 를 다루게 되었는데,
이 과정에서 백엔드로서의 JS, TS 를 깊게 배우고 조사했다.
이 때 정말 신경쓰이는 요소가 있었는데, 컴파일 결과 JS 로 실행되는 NestJS 가 Spring 급의
퍼포먼스를 낼 수 있는가에 대한 집착이었다.
JavaScript 는 인터프리터 언어이기 때문에, 런타임 환경에서 컴파일 된다.
시간이 지나며 JavaScript 의 인기와 커뮤니티에 알맞는 최적화를 가지게 되었지만,
결국 이미 바이너리 수준으로 컴파일 된 Java 의 JAR 파일을 이기기에는 분명한 한계를 가진다고 생각했다.
그럼에도 Node 환경에서의 백엔드 서비스가 나쁘지 않은 이유는, 결국 프로그램 실행 환경이 "비동기적"
이기 때문이었다.
요약하자면, Spring 은 비즈니스 로직을 처리하기 위해 스레드가 할당되고, 이 스레드들은 메인 로직이 가진
자원을 서로 공유하며 처리한다. 즉, 각각의 스레드는 동기적으로 작업된다.
그러나, Node 환경은 기본적으로 스레드가 1 개이다. 정확히 말하자면, "코드 실행을 기억하기 위한 콜스택이 1개다" 라고 하겠다.
(그 외의 몇 개의 스레드들은 JS 를 파싱하고 AST 로 만들거나, 바이트코드로 해석하기 위한 스레드)
Node 는 단일 스레드 환경에서 사용자의 비즈니스 로직을 처리한다. 이렇게 되면 당연히 Java 환경보다 느리겠지만,
단일 스레드 환경에서 동시다발적인 요청(이벤트) 를 처리하기 위해 "비동기" 환경을 도입한다는 것이다.
이 덕분에, 어떠한 요청도 막히지 않고 일단 받아들여주는 것이다.
비동기 덕분에, 모든 요청이 동시에 처리 될 수는 있었지만, 여전히 단일 스레드라는 점이 성능에 발목을 잡힌다.
따라서, 내가 조사했던 것은 Worker 라는 라이브러리였는데, 이는 서브 스레드를 만들어서
계산 로직을 분리 해 줄 수 있게 해 주었다.
작성했던 글
https://codecreature.tistory.com/201
그런데, 문제가 하나 있었다.
싱글 스레드 비동기 이벤트 Driven I/O 방식은 공유 자원이 없어
다른 스레드가 특정 리소스를 놓아줄 때 까지 기다릴 필요가 없다는 장점이 있다.
여기서 질문을 해 보자면, 그렇다면, 이 환경에서 새로운 스레드를 생성한다는 것을 무엇을 의미할까?
그렇다. 새로운 스레드의 실행을 위한 격리된 환경을 새로 만들어 주어야 한다는 것이다.
그래도 이전과는 나아진 것이,
만약에 WAS 를 제작 할 때, 계산이 많이 소비되는 로직이 있다면, 메인 스레드가 아니라,
Worker 스레드가 이를 분담하여 실행하고, 결과 값을 메인 스레드에 전달해 줄 수 있다는 점이다.
그러나, 나는 한 발자국 더 나아가기로 결정했다.
Node 와 브라우저의 JS 실행 환경에서는 웹 어셈블리 모듈을 붙일 수 있다.
이게 무슨 말이냐면, 새로 작성한 스레드가 JavaScript 로서 작동하는 것이 아니라,
아예 어셈블리 수준에서 작동한다는 것이었다.
이에 대해 작성한 글은,
https://codecreature.tistory.com/202
나는 웹 어셈블리 모듈 (.wasm) 을 만들기 위해서, AssemblyScript 라는 언어를 사용했다.
그런데, 오히려 C 언어로 만드는 것 보다 더 어렵다는 생각을 했다.
- AssemblyScript 라는 언어는 TypeScript 랑 비슷한 형태를 한다.
- 그러나, TypeScript 와는 달리 새로운 타입을 사용해야 한다. (Ex -
i32) - AssemblyScript 라는 언어를 사용하기 위해, 또 다른 복잡한 프로그램을 설치해야 한다.
- Node 프로젝트에 AssemblyScript 를 적용하기 위해 Node 프로젝트 설정값을 복잡하게 설정해야 한다.
- AssemblyScript 는 영향력 있는 언어가 아니다.
아마 처음 배우는 과정에서 설정하는 것이 매우 어렵기도 했지만,
이렇게 Node 환경을 최적화 하기 위해 노력 할 것이면,
이 노력으로 차라리 C 혹은 Rust, Zig 를 배우는 것이 낫지 않나? 라는 생각이 정말 많이 들었다.
그러나, JavaScript, TypeScript 가 가진 영향력과 웹에서의 개발 환경, 브라우저의 주요 언어를 생각 해 본다면,
애초에 JS 실행 환경에서 WASM 을 도입하는 것 자체가 희귀한 일이 아닐까 생각된다.
(그래픽 렌더링, 코드 편집기, 코인 거래소 사이트 등등 제외)
그러나, 나는 언어 자체의 한계점과 이를 돌파하기 위한 노력으로 여러 방면을 알게 되었다는 점에 집중했다.
다시 돌아와서,
프로그래밍 언어나 인프라 환경, 혹은 사용 어플리케이션에 따라 프로세스와 스레드의 사용 용도는 지속적으로 변화한다.
Saas 급의 개발 환경으로서의 진화가 나를 매우 편하게 만들어 주고,
매우 많은 개발 템플릿들로 개발 생산성을 높이는 것을 찬성하지만,
내가 만든 프로그램의 제작 고도화 과정에서 무조건적으로 프로세스와 스레드 관리 도메인이 나올 것이라고 생각한다.
애초에 나는 명령어를 통한 디렉토리, 파일 확인에 익숙하고,
Vim, Neovim 에서 Zed 에서 여러 명령어를 사용하고 있으니,
현재 내가 사용하고 있는 프로그램이 어떤 프로세스 ID 를 가지며,
몇 개의 스레드를 파생시켰는지 알 필요가 있었다.
프로세스와 스레드 개념을 꼭 알아야 하는 이유
현재 동시성 (Concurrency) 라는 모던 프로그래밍 개념이 강력하게 추진되고 있다.
이러한 개념을 필두로 한 프로그래밍 언어들도 나오고 있으며,
기존의 동기적 프로그래밍 방식의 단점을 해결하는 방식으로도 사용되고 있었다.
Golang 은 동시성 개념을 탑재하여 스레드 몇천개를 순식간에 만들 수도 있다던데,
왜 이런 현상이 놀라운 것인지 나도 알고 싶었다.
그리고, 나는 개인적으로 개발 템플릿에 잡아먹히고 싶진 않았다.
프로세스란?
먼저, 위키백과를 통해 프로세스의 사전적 정의를 살펴보자 :
프로세스 는, 컴퓨터에서 연속적으로 실행되고 있는 프로그램이다.
종종, 스케줄링의 대상이 되는 작업(Task) 라는 용어와 거의 같은 의미로 사용된다.
여러 개의 프로세서를 사용하는 것을 멀티프로세싱(Multi-Processing) 이라고 부르며,
같은 시간에 여러 개의 프로그램을 띄우는 "시분할" 방식을 멀티태스팅(Multi-Tasking) 이라고 한다.
이러한 프로세스 관리는, 운영 체제의 중요한 부분이 되었다.
위의 정의를 보고 든 생각이, 대학교에서 들은 운영체제 과목이 아직 내 머리속에 남아있구나 하는 것이었다.
각각의 프로세스를 효율적으로 관리하기 위해 박사 분들이 열심히 프로그램을 관리하고 있으시다 들었는데,
정말 경의를 표한다.
프로세스의 상태
먼저 커널 (kernel) 이라는 것을 알아야 하는데,
시스템의 "모든 것" 을 완전히 제어(control) 한다.
운영 체제의 특정 작업 및 응용 프로그램(Applications) 수행에 필요한 여러 가지 서비스를 제공한다.
커널은 보안, 자원관리, 추상화를 수행한다.
커널은 프로세스에 처리기(프로세서) 를 할당하며, 이를 스케줄링 (Scheduling) 이라고 한다.
또한, 커널은 시스템 자원을 제어하기 때문에, 여기에 접근하거나 상호작용할 수 있는 인터페이스를 제공한다.
커널은 위와 같은 작업 중, 프로세스를 관리하기 위해,
- Ready Queue = 준비 큐 : 준비 상태인 프로세스들을 큐로 관리
- Waiting Queue = 대기 큐 : 대기 상태인 프로세스들을 큐로 관리
- Running Queue = 실행 큐 : 실행중인 프로세스들을 큐로 관리
이러한 자료구조를 가지고 있으며, 이를 이용하여 프로세스의 상태를 관리한다.
참고로 Queue 는,
flowchart LR
New("새로운 엘리먼트")
Queue("Queue (큐)")
Old("오래된 엘리먼트")
New -- 추가 --> Queue -- Pop --> Old
이러한 형태를 가지며, 큐에서 엘리먼트를 뽑을 시, 가장 오래된 엘리먼트가 pop(추출) 된다.
그래서, 프로세스는 자체적으로 이러한 상태를 가진다.
- 생성 - Create : 프로세스가 생성되는 중이다.
- 실행 - Running : 프로세스가 CPU 를 차지하여 명령어들이 실행되고 있다.
- 준비 - Ready : 프로세스가 CPU 를 사용하고 있지는 않지만, 언제든지 사용할 수 있는 상태이다.
이는 CPU 가 할당되기를 기다리는 상태이며, 준비 Queue 의 프로세스 중 우선순위가 높은 프로세스가 CPU 를 할당받는다. - 대기 - Waiting : 보류(Block) 라고 부르기도 한다.
프로세스 입출력 완료, Signal 수신 등 특정 사건을 기다리고 있는 상태이다. - 종료 - Terminated : 프로세스의 실행이 종료된 것을 의미한다.
조금 넓게 보자면,
kernel(커널) 이 프로세스를 관리하며, 프로세스에게 이러한 상태들을 부여한다는 의미이다.
아마 그대로의 Queue 를 사용하는 것이 아니라, 우선순위 큐 (Priority Queue) 와 같은 자료구조를 사용 할 것 같다.
Process 의 상태 전이
flowchart TB
Swap-Wait("Swapped out and waiting (메모리에서 waiting)")
Swap-Block("Swapped out and blocked (메모리에서 blocked)")
Created("Created (생성)")
Waiting("Waiting (대기)")
Running("Running (실행)")
Blocked("Blocked (중단)")
Terminated("Terminated (종료)")
Created -- 준비 큐로 이동 --> Waiting
Waiting -- 실행 큐로 이동-dispatch --> Running
Running -- 시간제한됨-timeout --> Waiting
Running -- 보류-block --> Blocked
Blocked -- 깨우기-wakeup --> Waiting
Running -- 프로세스 종료 --> Terminated
Waiting -- 메모리에서 디스크로 이동 --> Swap-Wait
Swap-Wait -- 디스크에서 ready 로 이동 --> Waiting
Blocked -- 메모리에서 디스크로 이동 --> Swap-Block
Swap-Block -- 이벤트 발생 후 Blocked 로 이동 --> Blocked
위키백과의 그래프를 참조했는데, waiting 과 block 키워드의 의미가 조금 겹치는 것이 있어,
그래프 상에서 내가 배운 운영체제 지식과 결합하여 상세히 적어보았다.
먼저, Created 된 프로세스는 Ready(준비) 큐로 들어간다.
Dispatch (디스패치)
준비 큐의 맨 앞 프로세스가 CPU 를 점유하게 되는 것을 의미한다.
준비 상태에서 실행 상태로 바뀌는 것을 dispatch 라고 한다.
dispatch (processname) : ready -> running
Block (보류)
실행 상태의 프로세스가 허가된 시간을 다 쓰기 전에, 입출력 동작을 필요로 할 경우,
프로세스는 CPU 를 스스로 반납하고 보류(Blocked) 상태로 넘어 간다.
block (processname) : running -> blocked
wakeup (깨우기)
입출력 작업을 기다리며 Block 되었던 프로세스가,
입출력 작업 종료 등 특정 사건이 일어났을 때, 보류(block) 에서 준비(waiting and ready) 상태로 넘어가는 과정을 의미한다.
wakeup (processname) : blocked -> ready
timeout (시간제한)
운영체제의 커널은 process 가 processor 를 독점하지 못하게 Clock Interrupt 을 둔다.
따라서, 프로세스는 일정 시간 동안만 프로세서를 점유 할 수 있게 만든다.
timeout (processname) : running -> ready
대략적인 프로세스의 상태와 전이는 이렇다.
이것 말고도 Swapped out and waiting, Swapped out and blocked 상태가 있는데,
이는 위키백과 (프로세스) 에 들어가 보면 자세히 알 수 있다.
프로세스의 정보 자료구조 (PCB)
현재 작업중인 프로세스 리스트와 상세 정보를 볼 수 있는 것도 이러한 자료구조 덕분이라고 생각한다.
프로세스 제어 블록 (PCB) 라고 부르는데, Process Control Block 이라고 부른다. 혹은 (Task Control Block)
운영체제마다 PCB 에 탑재되는 항목이 조금씩 다를 수 있다고 한다.
하지만, 일반적으로 다음과 같은 정보를 포함한다고 한다.
Process ID(PID) - 프로세스 식별자 ID
Process State - 프로세스의 현재 상태
- 생성 (Create)
- 준비 (Ready)
- 실행 (Running)
- 대기 (Waiting)
- 완료 (Terminated)
- 유예 준비상태 (Suspended Ready) - Swapped out and Waiting (disk 에 존재)
- 유예 대기상태 (Suspended Wait) - Swapped out and Blocked (disk 에 존재)
Program Counter - 프로그램 계수기
이 프로세스가 다음에 실행 할 명령어의 주소를 가르킨다.
CPU Register or 일반 레지스터
CPU 스케줄링 정보
- 우선 순위
- 최종 실행 시각
- CPU 점유시간
- 등등
메모리 관리 정보 - 해당 프로세스의 주소 공간 등등
Process 계정 정보 - 페이지 테이블, 스케줄링 큐 포인터, 소유자, 부모 프로세스 등
입출력 상태 정보 - Process 에 할당된 입출력장치 목록, 열린 파일 목록 등등
Thread 란?
특히, 개발자는 응용 어플리케이션을 가장 많이 다루게 된다고 개인적으로 생각하는데,
특정 언어를 통해 프로세스 까지는 건들기 힘들더라도, 스레드는 쉽게 생성 할 수 있다.
이는 사용자의 컴퓨팅 리소스를 보호함을 위해 프로세스를 보호한다고 들었다.
스레드 또한, 위키백과의 정의를 통해 시작하자.
정의
스레드(thread) 는, 프로세스 내에서 실행되는 흐름의 단위를 말한다.
일반적으로 하나의 프로그램은 하나의 스레드를 가지고 있지만,
프로그램 환경에 따라 둘 이상의 스레드를 동시에 실행 할 수 있다.
이러한 실행 방식을 "멀티스레드" (Multi-Thread) 라고 한다.
Process 와 Thread 는 무엇이 다를까?
멀티 프로세스와, 멀티 스레드는 여러 흐름이 동시에 진행된다는 공통점을 가진다.
하지만, 멀티 프로세스는 독립적인 환경에서 각각 실행되며,
멀티 스레드는 소속되는 프로세스의 환경(메모리) 를 공유하여 사용할 수 있다.
그리고, 프로세스 전환보다 스레드 간의 전환이 빠르다고 한다.
멀티스레드의 장점은, CPU 가 여러 개일 경우, 각각의 cpu 가 스레드를 나눠 담당하는 방식으로 속도를 올릴 수 있다.
이러한 방식으로 실제 시간상으로 동시에 수행 될 수 있다고 한다.
단점으로는, 각각의 스레드 중 어떤 것이 먼저 실행될지 그 순서를 알수 없다고 한다.
프로세스 내부의 스레드 실행 환경
| 시간 | >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
----------
| | |
| | 스레드1 |~~~~~~~| |~~~~~~~~~~~~
| 프로세스 | |
| |-------------------------------------------------
| | |
| | 스레드2 | |~~~~~~~~~~~~~~~~~~~~|
| | |
하나의 프로세스에서 실행되는 2 개의 스레드 환경은 이렇게 표현 할 수 있다.
스레드의 종류
공식 문서를 보면서 든 생각은, 내가 다룬 스레드는 아마 대부분 사용자 레벨 스레드일 것이라고 생각한다.
한번 2 가지 스레드 종류를 보자.
1. 사용자 레벨 스레드
사용자 스레드는 커널 영역의 상위에서 지원하며, 사용자 레벨의 라이브러리를 통해 구현된다.
이 라이브러리는 스레드의 생성 및 스케줄링 등등에 관한 관리 기능을 제공한다.
장점은 속도가 빠르며,
단점으로는 스레드 중 하나의 스레드가 중단되면, 모든 스레드가 중단된다.
2. 커널 레벨 스레드
이는 사용자 라이브러리가 관리하는 것과는 다르게, 커널 자체에서 직접 스레드를 관리한다.
장점은 스레드 중 하나가 중단되더라도 다른 스레드를 계속 실행시켜주며, 다중 processor 에 스레드를 할당하기도 한다.
단점은, 사용자 스레드에 비해 생성 및 관리가 느리다.
스레드 데이터
1. 스레드의 기본 데이터
스레드 또한 Process 처럼 실행 흐름이므로, 실행과 관련된 데이터가 필요하다.
일반적으로 스레드는 이러한 정보를 가진다.
- Thread ID
- Program Counter
- Register Set
- Stack
2. 스레드의 특정 데이터
위의 기본 데이터 외에도 스레드의 추가 정보를 필요로 하는 경우가 있는데,
이는 스레드가 "트랜잭션" 을 처리 할 경우이다.
이 때, TSD 를 필요로 하는데, Thread Specific Data 의 약자이다.
프로세스와 스레드는 실제로 생각 한 것처럼 동작하지 않는다.
나는 위에서 1 개의 프로세스에 2 개의 스레드를 할당 했을 때 시간 분배가 어떻게 일어나는지 그렸다.
| 시간 | >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
----------
| | |
| | 스레드1 |~~~~~~~| |~~~~~~~~~~~~
| 프로세스 | |
| |-------------------------------------------------
| | |
| | 스레드2 | |~~~~~~~~~~~~~~~~~~~~|
| | |
실제 기기에서 스레드는 위의 그래프처럼 동작하기도, 동작하지 않기도 한다.
왜 그럴까?
프로세스는 스레드 정보를 추상화 한 정보이다.
프로세스는 최소 1 개의 스레드를 가지고 실행된다.
프로세스는 내가 위에 작성했던 "Process 의 상태 전이" 를 따라서 운용된다.
그런데, 이 그래프를 따르는 실제 단위는 사실상 Thread 이다.
물론, 프로세스가 위의 그래프를 따르기 때문에, 소속된 스레드들도 동일한 상태를 가지게 된다.
어떤 것이 생각 한 것 처럼 동작하지 않는다는 걸까?
먼저, 프로세스 내부의 스레드는 위의 그래프를 기반으로 운용되는 것이 사실이다.
그런데, 내가 떠올린 질문이 있었다.
"하나의 프로세스에 스레드를 몇백개 할당한다면, 하나의 스레드에서 처리하던 것 보단 빠르던데?"
여기서 나는 스스로 함정에 빠졌는데, 프로세스 내부의 스레드는 무조건 위의 형식을 따른다는 전제를 깐 것이다.
도대체 의문을 해소하지 못하여 이를 GPT 에게 직접 물어보아 의문을 해결했다.
현대 컴퓨터들은 대부분 멀티 프로세서를 사용한다.
즉, cpu 코어가 1 개만 존재하는 것이 아니라는 것이다.
만약에 특정 프로세스에 너무 많은 스레드가 할당된다면,
프로세스 내부의 스레드를 다른 프로세스에 할당하여 해결하는 것이 아니라,
프로세스 내부의 스레드들의 관계를 "그대로" 유지하되,
커널이 여러 스레드들을 프로세서에 할당하여 계산한다는 것이었다.
이렇게 된다면, 하나의 프로세스를 정확히 콕 집어서 그 안에서 시간 분배를 하여 계산하는 것이 아니라,
멀티 프로세서를 통하여 멀티 프로세스를 수행하는 효과를 얻을 수 있는 것이다.
여기서 내가 확실히 알게 된 것은, 프로세서는 물리 개념으로서, 프로세스는 로직 개념으로서 분리되어 있다는 것이다.
프로세스는 물리적으로 계산하는 기계이며, 프로세스는 계산해야 하는 Task 를 의미하는 정보이다.
프로세스와 스레드를 볼 수 있는 방법은 무엇일까
우리는 손쉽게 프로세스를 볼 수 있다.
윈도우 사용자들은 "작업 관리자"를 통하여 볼 수 있고,
맥 사용자들은 "활성 상태 보기" 를 통하여 볼 수 있다.
혹시 모를 리눅스 GUI 사용자들도 쉽게 볼 수 있는 프로그램이 있을 것이다.
그러나, 클라우드를 통한 컴퓨팅 서비스가 매우 활성화되었으며,
Docker, Kubernetes 를 통한 컨테이너 배포 방식이 매우 보편화 되어 있다.
여기서 우리가 어플리케이션을 배포했을 때, 위와 같은 프로그램과 서비스를 제외한다면,
우리가 프로세스가 정상적으로 진행되고 있는지 알 방법이 있을까?
따라서, 나는 명령어를 통해 현재 탑재된 프로세스 중에서, 내가 원하는 것만 볼 수 있는 방법을 알아보기로 결정했다.
프로세스와 스레드 정보를 명령어 탭에서 확인해 보자
현재는 커널 수준에서 Thread ID 를 보호하여 막는다고 한다.
따라서, 이제는 실행된 Process ID 와, 파생된 스레드들을 볼 수 있다.
예를 들어, PID 가 3 이고, 해당 프로세스에서 5 개의 스레드가 파생되었다면,
PID 를 가진 줄이 6 줄이다. (1줄 : 프로세스, 5줄 : 스레드)
참고로, 리눅스, 윈도우, 맥, 등등 각자의 운영체제에서 가지는 단축키와 명령어가 다를 수 있다.
예를 들어, 리눅스에서는 프로세스와 연관 스레드를 보기 위해,
$ ps -eLf
라는 명령어를 사용하지만,
Mac 에서는 현재 -L 옵션이 불가하다.
맥에서는 옵션이 -M 으로 매칭되어 있다.
따라서,
Mac 명령어 전용
$ ps -eMf
...
...
거의 수백줄?
우리는 특정 프로세스에서 파생된 스레드의 개수와 현황을 파악하고 싶어한다.
이렇게 명령어를 입력하면, 특히 백그라운드 프로그램이 많은 로컬 기기의 특성상
원하는 정보를 얻기 굉장히 힘들다.
그래서 대부분의 기기에 존재하는 기본 명령어가 있는데, 바로 grep 이다.
우리는 명령어 파이프 (|) 를 사용하여, 기존에 나온 문자열 결과물을 grep 으로 넘겨서,
원하는 PID 를 얻을 것이다.
WAS 프로그램 세팅
나는 NestJS 프로그램을 실행시켜놓고, 실제 실행되고 있는 Process 와 스레드 정보를 얻을 것이다.
먼저, NestJS 프로젝트에 들어가서,
➜ mycv git:(main) ✗ npm run start
> mycv@0.0.1 start
> cross-env NODE_ENV=development nest start
development
[Nest] 8901 - 05/20/2025, 10:38:38 PM LOG [NestFactory] Starting Nest application...
[Nest] 8901 - 05/20/2025, 10:38:38 PM LOG [InstanceLoader] TypeOrmModule dependencies initialized +22ms
....
명령어를 통해 프로젝트를 실행시킨다.
먼저 알려주자면, 위의 8901 숫자는, 현재 실행되고 있는 NestJS 서버의 실제 PID 를 의미한다.
그리고, 이러한 명령어를 입력한다.
$ ps -e -o pid,ppid,comm | grep npm
-v 명령어는 맥에서 프로세스 기본 속성들을 나열하기 위해 존재하는데,
우리는 현재 보고자 하는 속성 pid, ppid, comm 만 검색하기 위해 -o 옵션을 사용했다.
이 옵션은 기본 속성 후에 추가로 붙여지거나, 해당 속성들만 노출된다.
grep npm 을 한 이유는, 우리가 npm run start 로 서버를 시작했기 때문이다.
pid: 자신의 프로세스 IDppid: 자신을 파생시킨 프로세스 ID - parent Process IDcomm: 이 프로세스가 실행 된 명령어
Result :
➜ ~ ps -e -o pid,ppid,command | grep npm
9374 8989 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn --exclude-dir=.idea --exclude-dir=.tox --exclude-dir=.venv --exclude-dir=venv npm
8869 7102 npm run start
npm run start 의 프로세스 ID 는 8869 이다.
그렇다면, 이를 검색 해 보자.
➜ ~ ps -e -o pid,ppid,command | grep 8869
9442 8989 grep --color=auto --exclude-dir=.bzr --exclude-dir=CVS --exclude-dir=.git --exclude-dir=.hg --exclude-dir=.svn --exclude-dir=.idea --exclude-dir=.tox --exclude-dir=.venv --exclude-dir=venv 8869
8869 7102 npm run start
8884 8869 node /Users/<중간 디렉토리들>/nestjs/mycv/node_modules/.bin/cross-env NODE_ENV=development nest start
이를 통해, pid, ppid 가 8869 인 문장을 잡을 수 있다.
실제로, 본인은 프로젝트의 package.json 에 scripts 부분에 저렇게 정의 해 놓았다.
이제 끝이 나올 때 까지 부모 프로세스 PID 를 이용하여 가보자.
➜ ~ ps -e -o pid,ppid,command | grep 8884
8884 8869 node /Users/gongdamhyeong/intelliJ/udemy/nestjs/mycv/node_modules/.bin/cross-env NODE_ENV=development nest start
8885 8884 node /Users/gongdamhyeong/intelliJ/udemy/nestjs/mycv/node_modules/.bin/nest start
➜ ~ ps -e -o pid,ppid,command | grep 8885
8885 8884 node /Users/gongdamhyeong/intelliJ/udemy/nestjs/mycv/node_modules/.bin/nest start
8901 8885 node --enable-source-maps /Users/gongdamhyeong/intelliJ/udemy/nestjs/mycv/dist/src/main
➜ ~ ps -e -o pid,ppid,command | grep 8901
8901 8885 node --enable-source-maps /Users/gongdamhyeong/intelliJ/udemy/nestjs/mycv/dist/src/main
마지막까지 자식 프로세스를 추적 해 보니,
우리가 처음에 봤던
[Nest] 8901 - 05/20/2025, 10:38:38 PM LOG [NestFactory] Starting Nest application...
의 8901 PID 를 볼 수 있다.
그렇다면, 실제 실행중인 PID 들의 관계와 실행 상태를 보았으니,
스레드 개수와 상태를 알 수 있는 명령어를 입력 해 보자 :
➜ ~ ps -Mv 8901
USER PID TT %CPU STAT PRI STIME UTIME COMMAND TIME SL RE PAGEIN VSZ RSS LIM TSIZ %MEM
gongdamhyeong 8901 s004 0.0 S 31T 0:00.15 0:00.72 node -- 0:01.15 0 0 0 35689140 115456 - 0 0.7
8901 0.0 S 31T 0:00.00 0:00.01 0:01.15 0 0 0 35689140 115456 - 0 0.7
8901 0.0 S 31T 0:00.00 0:00.00 0:01.15 0 0 0 35689140 115456 - 0 0.7
8901 0.0 S 31T 0:00.01 0:00.04 0:01.15 0 0 0 35689140 115456 - 0 0.7
8901 0.0 S 31T 0:00.01 0:00.06 0:01.15 0 0 0 35689140 115456 - 0 0.7
8901 0.0 S 31T 0:00.01 0:00.05 0:01.15 0 0 0 35689140 115456 - 0 0.7
8901 0.0 S 31T 0:00.01 0:00.04 0:01.15 0 0 0 35689140 115456 - 0 0.7
8901 0.0 S 31T 0:00.00 0:00.00 0:01.15 0 0 0 35689140 115456 - 0 0.7
8901 0.0 S 31T 0:00.00 0:00.00 0:01.15 0 0 0 35689140 115456 - 0 0.7
8901 0.0 S 31T 0:00.00 0:00.00 0:01.15 0 0 0 35689140 115456 - 0 0.7
8901 0.0 S 31T 0:00.00 0:00.05 0:01.15 0 0 0 35689140 115456 - 0 0.7
8901 0.0 S 31T 0:00.00 0:00.00 0:01.15 0 0 0 35689140 115456 - 0 0.7
맨 위의 프로세스 행을 제외하면, 11 개의 스레드가 NestJS 서버 기동에 사용되고 있다는 것을 알 수 있다.
일단 예상되는 스레드는,
- 코드 실행을 수행하는 콜스택 스레드 1개
- libuv 스레드 4개
- GC 1개
- JIT 1개
- async hook X개
- 타입 source map 을 이용하여 체킹하는 스레드 x 개
이렇게 구성되어 있는 것 같다.
명령어를 통한 스레드 확인 후기를 보자면..
내가 한 방식은 절대로 정상적인 방식은 아닌 것 같다.
아마 ps, grep 외의 특별한 명령어 라이브러리를 통해 쉽게 펼칠 수도 있을 것이다.
하지만, 내가 아는 한의 명령어 사용법을 통해,
- 현재 PID 와 해당 PID 를 부모로 삼는 프로세스 추적
- 마지막에 도달한 실제 서버 Process 를 펼쳐 11 개의 스레드가 실행되고 있음을 알 수 있다.
S는 Sleep 을 의미하는데, 이는 실제로 서버에 특별한 이벤트가 없어서 대기하고 있는 중이다.R은 현재 실행중인 프로세스 혹은 스레드를 의미한다.- 더 많은 명령어와 옵션으로 쉽게 추적할 수 있을 것이다.
이번 글을 작성하며 배운 것
프로세스와 스레드의 정확한 의미,
그리고 사용자 라이브러리 단계에서 어떻게 사용되는지 배우게 되었다.
특히, 프로세스의 실행 단계가 아주 정확히는 스레드 자체라는 것에 살짝 충격을 얻었는데,
이는 프로세스 자체가 그렇게 실행이 되고,
스레드는 내부적으로 처리되는 특정 단계가 있을 것이라 예측했기 때문이다.
그리고, Java 의 Thread 와, Node.js 의 Worker 가
OS 스레드와 사용자 스레드로서의 1 : 1 매칭이라는 것 또한 알게 되었다.
나는 OS 스레드에 한정된 다중 사용자 스레드로서 매칭되어 M : 1 이 될 줄 알았다.
즉, 언어들이 따로 직접적으로 스레드를 생성할 시,
OS 스레드와 1 : 1 매칭시켜 커널 수준에서 스스로 최적화 하도록 만들었다는 것이다.
OS 는 사용자 스레드가 어떻게 실행되는지 알 수 없기 때문이다.
그리고, 이 글을 작성 한 계기는 프로세스와 스레드, 멀티프로세스, 멀티스레드 환경에 대한
정확한 이해를 위함이기도 했지만,
더 나아가 "동시성" 을 말하는 "Concurrency" 를 배우고 글을 작성하기 위함이었다.
요즘 현대적 언어들은 동시성을 탑재하거나, 추후 이를 지원하는 기능을 추가했다.
그 만큼 병렬 연산을 빠르게 하기 위해 진심인 것인데,
나는 동시성이라는 단어를 들어만 보고 개념만 조금 알고 있을 뿐, 정확히는 알지 못했다.
먼저, 이 글을 작성하게 되면서 알게 된 것은,
OS 스레드 M 개와, 사용자 스레드 N 개와 매칭되어 계산한다는 것이다.
기존이 스레드는 1 : 1 이었던 반면,
언어 수준에서 N 개의 사용자 스레드를 생성하고,
이를 OS 스레드 M 개와 매칭시켜 최적화한다는 발상은 굉장히 신선했다.
그리고, 이번에 실제로 명령어 수준에서 프로세스를 검색하고,
부모 프로세스의 PID 를 통해 자식 프로세스를 탐색하는 과정은 명령어에 대한 이해도를 한층 끌어올려주었다.
조금 아쉬운 것은, 내가 ps 와 grep 에 대한 명령어 라이브러리 이해도가 높지 않아
편하게 검색하기 어려웠다는 점이다.
참고 사이트
위키백과 (프로세스)
https://ko.wikipedia.org/wiki/%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4
위키백과 (커널 - 컴퓨팅)
https://ko.wikipedia.org/wiki/%EC%BB%A4%EB%84%90_(%EC%BB%B4%ED%93%A8%ED%8C%85)
위키백과 (스레드 - 컴퓨팅)
https://ko.wikipedia.org/wiki/%EC%8A%A4%EB%A0%88%EB%93%9C_(%EC%BB%B4%ED%93%A8%ED%8C%85)
위키백과 (멀티스레딩)
https://ko.wikipedia.org/wiki/%EB%A9%80%ED%8B%B0%EC%8A%A4%EB%A0%88%EB%94%A9
위키백과 (프로세스 관리)
https://ko.wikipedia.org/wiki/%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4_%EA%B4%80%EB%A6%AC
'잡다 지식' 카테고리의 다른 글
| 내가 Node.js 에서 Spring 으로 백엔드 영역을 변경하는 이유 (1) | 2025.11.05 |
|---|---|
| 동시성 (Concurrency) 이란 무엇일까? - 부제 : 동시 실행이라는 착각 (4) | 2025.06.04 |
| Domain 과 SSL 에 대하여 (부제 : https 는 뭘까?) (0) | 2025.05.19 |
| 브라우저의 동작 과정과 기능들 (0) | 2025.05.15 |
| TypeORM CLI 와 데이터베이스 마이그레이션 (1) | 2025.05.10 |