Loading...
본문 바로가기
👥
총 방문자
📖
0개 이상
총 포스팅
🧑
오늘 방문자 수
📅
0일째
블로그 운영

여러분의 방문을 환영해요! 🎉

다양한 개발 지식을 쉽고 재미있게 알려드리는 블로그가 될게요. 함께 성장해요! 😊

CS

[NodeJs] os 관점에서 더 자세히 알아보자

by 꽁이꽁설꽁돌 2025. 4. 5.
728x90
반응형
     

목차

     

     

    운영체제에서 스레드를 공부하던 나는 문득 js가 단일 스레드라는 사실이 생각이 났고 더 파서 공부해보자 라는 생각이 들었다.

     

    각 언어의 스레드 방식

    각 언어들은 유저 레벨 스레드와 커널 레벨 스레드 방식을 이용하고 있는데 방식이 다양하다.

    요즘의 언어들에서 유저 레벨 스레드만 사용하는 언어는 드물다 (여러 스레드의 병렬 처리 때문)

     

    C / POSIX

    • pthread 사용 → 1:1 모델
    • 커널이 직접 스케줄링 (커널 스레드)
    • 커널 관여 O

    Python

    • threading 있음 → 내부적으로는 커널 스레드 (1:1)
    • 하지만 GIL 때문에 동시 실행은 안 됨 (I/O는 병렬, CPU는 직렬)
    • 커널 관여 O

    Java (HotSpot)

    • OS 커널 스레드를 직접 사용 → 1:1
    • 커널 관여 O

    Node.js

    • JS 실행은 싱글 커널 스레드
    • libuv로 유저 레벨에서 비동기 관리
    • 필요 시 워커 스레드 → 커널 스레드
    • 커널 관여 O (제한적)

    Go (Golang)

    • 고루틴 = 유저 레벨 스레드
    • Go 런타임이 고루틴을 커널 스레드에 스케줄링
    • → M:N 모델
    • 유저가 관리하지만 커널 스레드도 적극 활용 → 관여 O

    Erlang / Haskell

    • M:N 유저 스레드 모델
    • 자체 스케줄러로 매우 가벼운 스레드 수천 개 관리
    • OS 스레드에 바인딩되기도 함
    • 유저 + 커널 협력

     

    NodeJs와 OS의 관계

    NodeJs는 싱글 스레드로 동작하는 언어이다. js도 결국에는 os위에서 돌아가는 프로그램이므로 

    크롬같은 브라우저 프로세스가 os위에서 동작하게 된다. 

     

    -> js자체는 os랑 직접 상호작용을 하지 않지만 js를 실행하는 런타임은 os 위에서 동작하게 된다.

    즉 파일 접근, 네트워크, 타이머 등은 결국 os의 기능(시스템 콜)을 통해 처리가 된다.

     

     

    그렇다면 우리가 작성한 NodeJs의 코드들은 프로세스 이미지의 영역 중 유저 데이터 영역에 들어가는 것인가?

     -> 엄밀히 말하면 코드가 기계어로 변환되어 user data 영역에 들어가는 것이다.

     

     

     

    NodeJs는 싱글 스레드 언어

     

    이벤트 루프의 역할

    여기서 이벤트 루프는 메인 스레드 겸 싱글 스레드로써 비지니스 로직을 수행한다.

    수행 도중에 io 작업을 만나면 커널 비동기  또는 자신의 워커쓰레드풀에게 넘겨주는 역할까지 한다.

    그렇다면 외부 api콜을 하는 것은 커널 비동기 또는 자신의 워커쓰레드가 수행한다. 동시에 많은 요청이 들어온다고 해도

    1개의 이벤트 루프에서 처리를 하는 데 이것을 도대체 어떻게 해결한 것일까?

     

     

    바로 libuv 덕분이다. 

     

    libuv란?

    libuv는 윈도우 커널, 리눅스 커널을 추상화해서 wrapping하고 있다.

    nodejs는 기본적으로 libuv 위에서 동작하며 node 인스턴스가 뜰 때 libuv에는 워커 스레드풀이 생성된다.

    위에서 블로킹 작업(api 콜, DB read/write 등)들이 들어오면 이벤트 루프가 uv_io에게 내려준다. 

    libuv는 커널단에서 어떤 비동기 작업들을 지원해 주는지 알고 있기 때문에 그런 종류의 작업들을 받으면 

    커널의 비동기 함수들을 호출한다. 작업이 완료되면 시스템콜을 libuv에게 던져준다. libuv 내에 있는 

    이벤트 루프에게 콜백으로서 등록된다. libuv의 워커쓰레드는 커널이 지원안하는 작업들을 수행한다.

    대표적인 예로 소켓 작업류는 커널들이 이미 비동기로 지원하지만, 파일시스템쪽 작업은 지원하지 않는데

    이럴때 libuv의 쓰레드가 쓰인다.

     

     

    ?? 그렇다면 여기서 의문점이 생긴다.. 분명 nodeJs는 단일 스레드 기반이라고 했는데..

     

     

    NodeJs가 단일 스레드 런타임이라고 불리는 이유는 javascript 실행 컨텍스트는 하나의 유저 스레드에서 돌아가고

    개발자는 일반적으로 스레드 관리 없이 async/ await 또는 callback으로 작성하게 된다.

    즉 개발자 입장에서 직접적인 멀티스레드의 사용이 없고 실제 내부적으로 돌아갈 때는 멀티 스레드를 활용한다.

     

     

    그렇다면 어떻게 이벤트 루프에서 여러개의 이벤트를 처리할까?

     

     

    이벤트 루프 이벤트 처리 예제

    fs.readFile(__filename, () => {
        setTimeout(() => {
            console.log('A');
        }, 0);
        setImmediate(() => {
            console.log('B');
        });
    });

     

    1. fs.readFile 라는 블로킹 작업을 만난 시점에 이벤트 루프는 워커쓰레드에게 작업을 넘긴다.

     

    2. 워커 스레드가 작업을 완료한 뒤 i/o callback 영역 큐에 콜백을 등록한다.

     

    3. 이벤트 루프가 io callback 영역을 실행할 때 콜백을 poll 영역의 큐에 등록한다.

     

    4. 이벤트 루프가 poll 영역을 실행할 때, 큐에 1개가 있으므로 이것을 실행한다.

     

    5. (콜백 내부) 2라인에서 setTimeout()이므로 다시 timers 영역에 넣고 5라인으로 간다.

     

    6. (콜백 내부) 5라인에서 setImmediate()이므로 check 영역에 넣는다.

     

    7. 이벤트루프가 poll 큐를 비우고 다음 실행영역인 check 영역으로 간다. check 영역의 큐에는 들어있는

    B를 콘솔에 찍는다. check 영역의 큐를 비우고 다시 while 문의 시작지점으로 간다.

     

    8. 이벤트 루프가 timers 영역을 호풀한다. uv_run_timers()는 setTimeout()의 콜백을 poll 큐에 등록한다.

     

    9. 이벤트 루프가 2번째로 poll 영역을 실행한다. 큐에 1개가 있으므로 이걸 실행하고 A를 찍는다.

     

    10. node 프로세스가 반환되고 끝난다.

     

     

     

    워커 스레드 더 자세히 이해하기

    노드는 단일 스레드에서 실행되고, 이벤트 루프에는 한 번에 하나의 프로세스만 발생합니다.

    하나의 코드, 하나의 실행, (코드는 병렬로 실행되지 않는다).

     

    이 점은 매우 유용한데, 왜냐하면 당신이 동시성 문제에 대한 걱정 없이 자바스크립트를 사용하는 방법을 단순하게 만들어주기 때문이다.

     

    그러나 모든 것과 마찬가지로 단점도 있다.

    만약 인 메모리에서 발생하는 대규모 데이터 세트의 복잡한 계산같은 CPU 자원을 많이 사용하는 코드가 있으면, 이 코드가 다른 프로세스가 실행되는걸 차단할 수도 있다.

    마찬가지로, CPU 자원을 많이 사용하는 코드가 있는 서버에 요청하는 경우, 이 코드가 이벤트 루프를 차단하고 다른 요청들이 처리되지 않게 할 수도 있다.

     

    CPU 작업과 I/O 작업을 구분하는 게 중요히다.

    앞에서 말했듯이, Node.js 코드는 병렬로 실행되지 않습니다. 

    오직 I/O 작업만 비동기식으로 실행되므로, 병렬로 실행된다.

     

    그래서 워커 스레드는 I/O 집약적인 일에는 별로 효과적이지 못한데, 왜냐하면 비동기적 I/O 작업이 워커가 하는 것보다 더 효율적이기 때문이다.

    워커의 가장 중요한 목표는 I/O 작업이 아닌 CPU 집약적인 작업의 퍼포먼스를 향상시키는 것이다.

     

     

    -> 즉 i/o와 관련된 작업은 이벤트 루프의 단일 스레드에서 처리하고 cpu와 관련된 작업은 워커 스레드에서 처리한다.

     

     

     

    그렇다면 유저 레벨 스레드에서 라이브러리를 통해 멀티 스레드를 쓸 방법은 없을까?

     

     

    유저 레벨에서의 멀티 스레드 사용 (service worker)

     

    Web Workers

    • 여러 개의 Worker를 등록해 사용하는 것이 가능하다.
    • 단, 브라우저 탭이 닫히면 Worker 역시 종료된다.

    Service Workers

    • 도메인 당 하나의 Worker만 생성해 사용할 수 있다.
    • 브라우저 탭이 닫히더라도 백그라운드에서 계속 실행할 수 있다.
    • 네트워크 요청을 가로채는 것이 가능하고, Push API를 통해 이벤트를 수신하는 것이 가능하다.

     

    대표적인 사례 msw

     

    프론트엔드 애플리케이션을 테스트 할 때 많이 사용하는 MSW가 바로 Service Worker를 이용하고 있다.

    MSW는 클라이언트의 모든 네트워크 요청을 Service Worker를 이용해 가로채고, 이를 미리 정의된 Mocked Response로 바꾸어 내려주도록 동작하고 있다.

     

     

    더 알고 싶으면 아래를 참고하자

    https://be-senior-developer.tistory.com/89

     

    MSW 정의 및 기본 세팅 방법

    목차  Mocking의 정의Mocking은 테스트를 독립시키기 위해 의존성을 개발자가 컨트롤하고 검사할 수 있는 오브젝트로 변환하는 테크닉쉽게 말해서테스트 코드를 작성하다보면 가끔 실제로 작성할

    be-senior-developer.tistory.com

     

     

    반응형