Node.js의 내부

2024. 12. 5. 16:30Programming Language/Node.js

01. Node.js가 작업을 처리하는 방법

런타임 환경에서 자바스크립트로 처리할 수 있는 작업은 V8 엔진이 처리를 할 수있으나 파일을 읽는 것과 같은 작업은 처리하지 못한다. 그래서 이런 부분은 libuv를 통해서 작업을 한다.

그리고 이 둘을 연결(binding)해주는 것이 Node.js이다.

 

Node.js의 API는 많은 기능을 갖고 있는데 파일 시스템이나 crypto 암호화 처리등의 기능은 V8엔진이 처리할 수 없기 때문에 V8이 이런 기능이 필요하다 싶으면 Node.js의 API를 호출한다.

그러면 그걸 Node.js가 확인하고 자바스크립트로 처리할 수 있는 부분을 제외하고는 libuv 한테 전달해서(Node.js의 바인딩을 사용해서) 처리하게 해준다. 

이 libuv는 비동기적으로 input/output을 사용하기 때문에 이 시간동안 자바스크립트는 다른 일을 진행할 수 있다.

그리고 하나 특이점은 libuv는 운영체제에 맞게 파일 시스템을 처리를 해준다(원래 운영체제에 맞게 처리 방법을 다르게 해야함). 

 

구체적으로 설명하자면 

  1. JavaScript 코드 실행 - 사용자가 작성한 JavaScript 코드는 Node.js의 V8 엔진에 의해 실행된다.
  2. V8이 처리하지 못한 코드를 Node.js 바인딩 사용 - Node.js는 JavaScript와 C/C++ 코드 사이의 인터페이스 역할을 하는 바인딩을 사용한다. 바인딩은 JavaScript에서 호출된 API를 C/C++ 함수로 변환한다.
    (변환의 과정은 JavaScript API 호출을 해당하는 C/C++ 함수로 매핑한다. 이 과정에서 JavaScript 데이터 타입을 C/C++ 데이터 타입으로 변환한다.)
  3. C/C++ 함수 실행 - 매핑된 C/C++ 함수가 실행되고 이 함수들은 대부분 Node.js의 내부 구현체인 src 디렉토리에 정의되어 있다
  4. libuv 사용 - C/C++ 함수 내에서 필요한 경우 libuv 라이브러리를 호출하여 작업을 수행한다.

바인딩은 JavaScript와 C/C++ 사이의 "통역사" 역할을 하며, C/C++ 코드를 직접 libuv로 전달하는 것이 아니라 C/C++ 함수를 실행하고, 그 함수 내에서 필요에 따라 libuv를 사용하는 구조이다.

02. Node 오픈 소스 코드를 통한 이해

이번엔 Node.js 프로젝트를 보면서 위 과정이 어떻게 진행되는지 확인해보자.

먼저 Node.js 오픈소스 프로젝트를 들어가보면 

https://github.com/nodejs/node

 

GitHub - nodejs/node: Node.js JavaScript runtime ✨🐢🚀✨

Node.js JavaScript runtime ✨🐢🚀✨. Contribute to nodejs/node development by creating an account on GitHub.

github.com

이렇게 오픈 소스 프로젝트의 내용이 나온다.

 

이 부분이 Node.js의 소스코드가 있는 부분인데 여기서 우리가 중요하게 볼 폴더는 lib과 src이다.

그래서 lib 폴더안에는 위에서 말했던 API들이 들어 있다.

이렇게 Node.js의 자바스크립트 부분이 lib폴더 내부에 존재하고 있고 src폴더에는 c/c++부분으로 lib폴더 내부에 C/C++의 코드의 호출이 필요한 API들(File System, HTTP, HTTPS 등..)의 실질적인 구현체들이 들어 있다.

그리고 src의 구현체들과 lib의 자바스크립트(C/C++을 호출해야만 하는)를 연결해주는 것이 Node.js Binding인 것이다.

 

만약 어떤 파일을 열고자 할때는 fs라는 API를 사용해야하는데 이 fs라는 API는 

이렇게 lib 폴더 내부에 존재하고 이 API를 사용해서 파일을 열어줄 수 있다.

이 함수는 아래와 같이 /lib/fs.js파일 내부에 구현이 되어 있는데

이 함수의 내부를 보면 

이 부분이 바로 src의 C/C++부분을 연결 시켜주는 바인딩 부분이다. 

더보기

Node.js의 내부 바인딩 메커니즘은 아래 과정을 통해 JavaScript에서 C++ 함수를 호출한다


1. 모듈 초기화: Node.js 시작 시 C++ 모듈들이 초기화되며, JavaScript와 C++ 함수 간 매핑이 설정된다.


2. internalBinding 사용: Node.js는 'internalBinding' 메커니즘으로 C++ 함수를 JavaScript에 노출시킨다.


3. fs.js와 C++ 연결: fs.js 모듈 로드 시 'internalBinding('fs')' 호출로 관련 C++ 바인딩이 로드된다.

('internalBinding('fs')' 호출은 실제로 JavaScript 코드와 C++ 코드를 연결시키는 역할을 한다. 이는 이미 설정된 매핑을 활성화하고 사용 가능하게 만드는 과정이다. 그리고 fs와 같은 내부 라이브러리의 경우, Node.js 내부에 이미 해당 모듈명과 관련 C++ 코드의 위치가 정의되어 있다. 'fs'라는 식별자는 Node.js 내부에서 src/node_file.cc 파일의 특정 함수들과 연결되도록 미리 설정되어 있다 )


4. 함수 매핑: 'binding.open'은 src/node_file.cc 파일의 'Open' 함수에 매핑된다.

internalBinding('fs')부분이 실행되면 src/node_file.cc 파일에서 해당 함수가 실행되면서 SetMethod를 통해서 바인딩이 시작된다. 이렇게 바인딩을 해주는 함수의 명칭은 모듈마다 다를 수 있다.


이 과정을 통해 JavaScript의 'binding.open' 호출이 적절한 C++ 함수를 실행하게 된다. 개발자는 이 매핑 과정을 직접 관리할 필요 없이 Node.js의 내부 메커니즘이 처리한다.

 

그래서 사실 바인딩을 하는 부분은 

이 부분이 실제 바인딩에 해당되고 

이 부분은 바인딩 되어 있는 open이란 함수를 호출하는 부분이다.

그리고 이 바인딩 부분은 src/node_file.cc 파일의 'Open' 함수에 맵핑 되어 있고 

이 내용중에서 libuv에게 파일을 열라는 함수를 호출해줘야 하는데 그 부분이 

여기서 uv_fs_open이란 함수로 libuv의 파일 시스템 open 함수이다.

이건 비동기적 호출에 해당하는 내용에 대한 부분이고 동기 호출은 

이 부분에 해당한다.

 

이게 fs API를 자바스크립트 - node.js binding - libuv까지의 연결되는 과정이다.

 

03. libuv

libuv라는 것은  비동기 및 이벤트 기반 I/O를 지원하는 크로스 플랫폼 라이브러리이다.

이 라이브러리는 윈도우, 맥이든 어떤 플랫폼에서 사용이 가능하다.

이 라이브러리는 C언어로 작성되어 있고 Node.js가 비동기식 이벤트 주도 아키텍처를 구현할 수 있도록 돕는다.

 

우리가 이전에 uv_fs_open을 봤었는데 이 함수 또한 libuv의 오픈소스에서 확인이 가능하다.

https://github.com/libuv/libuv

 

GitHub - libuv/libuv: Cross-platform asynchronous I/O

Cross-platform asynchronous I/O. Contribute to libuv/libuv development by creating an account on GitHub.

github.com

여기서 src 폴더가 우리가 확인하고자 하는 소스를 갖고있는 폴더로 대부분의 C코드가 들어 있는 부분이다.

src로 들어가보면 unix와 win이란 폴더가 보이는데 unix는 맥OS와 Linux를 처리해주는 부분이고 win은 windows를 처리하는 부분으로 따로 처리할 수 있도록 해놨기 때문에 Node.js를 사용할때는 따로 소스코드를 다르게 짜지 않아도 처리가 가능한 것이다.

 

unix에 먼저 들어가서 fs.c파일을 찾아가서 

uv_fs_open를 찾아가보면 

이렇게 해당 함수를 정의해둔 부분을 찾을 수 있다.

실제 이 부분에서는 파일을 열기 작업을 구현하지는 않고 아래의 uv__fs_open함수 내부에서 파일 열기를 구현한다.

그리고 그 결과값을 자바스크립트로 return해주는 부분이 바로 위에 부분이다.

 

윈도우도 fs.c파일 내부에서 그런 과정이 비슷하게 진행된다.(받아서 구현하고 그걸 자바스크립트로 return할 수 있도록해주는 과정)

그렇기 때문에 Node.js를 사용하는 사람은 크게 신경 쓰지 않고 사용할 수 있게 된다.

 

04. 비동기와 동기

프로그램을 작성할 때, 작업을 수행하는 방식에 따라 동기와 비동기로 나눌 수 있다. 이 두 가지 방식은 각각의 특성과 장단점이 있으며, 특정 상황에 따라 적합한 방식을 선택해야 한다.

 

1. 동기(Synchronous)
동기는 작업이 순차적으로 진행되는 방식을 의미한다. 즉, 한 작업이 완료되어야만 다음 작업이 시작된다. 이 방식은 코드의 흐름이 직관적이며 이해하기 쉽다.

 

장점
코드가 직관적이고 이해하기 쉽다.
디버깅과 유지보수가 용이하다.


단점
작업이 완료될 때까지 대기해야 하므로, 전체적인 성능이 저하될 수 있다.
긴 작업이 있을 경우 사용자 경험에 부정적인 영향을 미칠 수 있다.

 

2. 비동기(Asynchronous)
비동기는 작업을 요청한 후, 그 결과를 기다리지 않고 다음 작업을 진행하는 방식을 의미한다. 즉, 작업이 완료되기를 기다리지 않고 다른 작업을 동시에 수행할 수 있다.


예시
파일 읽기: 파일을 읽는 요청을 보낸 후, 프로그램은 파일 읽기가 완료될 때까지 기다리지 않고 다른 작업을 수행한다. 파일 읽기가 완료되면 등록된 콜백 함수가 호출되어 결과를 처리한다.
HTTP 요청: 서버에 HTTP 요청을 보내고 응답을 기다리는 동안 프로그램은 계속 실행된다. 응답이 오면 특정 함수가 호출되어 결과를 처리한다.


장점
사용자 인터페이스가 부드럽고 반응성이 뛰어나다.
긴 작업이 있는 경우에도 다른 작업을 동시에 수행할 수 있어 성능이 향상된다.


단점
코드의 흐름이 복잡해질 수 있으며, 이해하기 어려울 수 있다.
오류 처리가 복잡해질 수 있으며, 디버깅이 어려울 수 있다.

 

특성 동기 비동기
실행방식 순차적으로 실행 동시에 여러 작업 실행 가능
코드 흐름 직관적이고 간단함 복잡할 수 있음
성능 긴 대기 시간 발생 가능 높은 성능과 반응성 제공
오류 처리 간단함 복잡할 수 있음

 

동기와 비동기는 각각의 상황에 따라 적합하게 사용해야 한다. 간단한 작업이나 짧은 시간 내에 완료되는 작업에는 동기가 유리할 수 있지만, 사용자 경험과 성능을 고려해야 하는 경우에는 비동기가 더 효과적일 수 있다.

 

05. javascript는 동기 언어 

자바스크립트는 한 줄을 실행하고 다음 줄을 실행하는 동기언어이다.

근데 ajax, setTimeout등의 함수는 비동기적으로 사용했었는데 자바스크립트가 동기 언어라는게 무슨 이야기일까

ajax, setTimeout등의 함수는 자바스크립트의 부분이 아니라 브라우저에서 사용한다면 브라우저의 API를 사용한 것이고 REPL 환경에서 사용했다면 node.js의 API를 사용한 것이다.

 

결론적으로, 자바스크립트는 동기 언어이나 다른 무언가들의 도움을 받는다면 비동기로도 사용이 가능하다라는 것이다.

 

06. block, non-blocking in node.js

Node.js에서 블로킹(Blocking)과 논블로킹(Non-Blocking)의 개념은 비동기 프로그래밍을 이해하는 데 중요한 요소이다.

이 두 가지 방식은 프로그램의 실행 흐름과 성능에 큰 영향을 미친다. 

 

블로킹(Blocking)

블로킹은 한 작업이 완료될 때까지 다른 작업이 대기하는 방식을 의미한다.

다시말해 현재 작업이 끝나야만 다음 작업으로 넘어갈 수 있다.

이 방식은 주로 동기적인 처리 방식에서 나타난다.

  • 파일 읽기: 파일을 읽는 요청을 보낼 때, 해당 파일이 완전히 읽힐 때까지 프로그램은 멈춘다. 이 동안 다른 작업은 수행되지 않는다.
  • HTTP 요청: 서버에 데이터를 요청할 때, 응답이 올 때까지 기다리며 다른 작업을 하지 않는다.

 

장점

  • 예측 가능한 흐름: 코드가 순차적으로 실행되므로, 각 작업의 완료 시점을 쉽게 예측할 수 있다.
  • 간단한 구현: 코드가 직관적이며 이해하기 쉽다.

단점

  • 성능 저하: 긴 작업이 있을 경우, 전체 프로그램의 성능이 저하될 수 있다. 예를 들어, 파일을 읽는 동안 다른 요청이 처리되지 않으므로 대기 시간이 발생한다.
  • 확장성 문제: 동시에 처리할 수 있는 요청의 수가 제한적이다. 여러 사용자가 동시에 요청할 경우, 블로킹으로 인해 서버가 느려질 수 있다.

 

논블로킹(Non-Blocking)

논블로킹은 한 작업이 완료될 때까지 기다리지 않고, 다른 작업을 수행할 수 있는 방식을 의미한다.

작업을 요청한 후 결과를 기다리지 않고 다음 작업으로 넘어간다.

  • 파일 읽기: 파일 읽기 요청을 보낸 후, 파일이 완전히 읽히지 않아도 다른 코드를 실행할 수 있다. 파일 읽기가 완료되면 콜백 함수가 호출되어 결과를 처리한다.
  • HTTP 요청: 서버에 데이터를 요청한 후, 응답을 기다리는 동안 다른 처리 작업을 수행할 수 있다. 응답이 오면 등록된 콜백 함수가 호출된다.

장점

  • 성능 향상: 다른 작업을 동시에 처리할 수 있으므로 시스템의 성능이 개선된다. 특히 I/O 작업에서 유리하다.
  • 확장성: 여러 사용자의 요청을 동시에 처리할 수 있어 서버의 확장성이 높아진다.


단점

  • 복잡한 코드: 비동기적으로 처리되므로 코드가 복잡해질 수 있으며, "콜백 헬" 현상이 발생할 수 있다.
  • 예측 어려움: 작업의 완료 순서가 보장되지 않기 때문에 예측하기 어려운 동작을 할 수 있다.

Node.js는 비동기 이벤트 기반 아키텍처를 채택하고 있으며, 주로 논블로킹 방식을 사용하여 높은 성능과 확장성을 제공한다. 블로킹 방식은 간단하지만 성능 저하와 확장성 문제를 초래할 수 있다. 따라서 Node.js에서는 가능한 한 논블로킹 방식을 활용하여 효율적인 애플리케이션을 개발하는 것이 중요하다

 

07. 프로세스 및 스레드

곧 Node.js가 한번에 여러가지 일을 처리할 수 있게 하는 방법에 대해서 이야기 할건데 그전에 스레드에 대한 개념을 알고 있으면 이해가 쉬워 질것이기에 스레드에 대한 개념을 설명하고자 한다.

 

먼저 windows에서 프로세스마다의 스레드를 확인하기 위해서는 작업 관리자를 열여주고(ctrl + alt + delete) 

상단 항목에서 세부 정보를 선택한다 

그러면 아래와 같은 창이 뜨는데 

여기서 이름 / PID/ 상태 .. 등등 메뉴 부분에 마우스 오른쪽 클릭을 누르면 

이렇게 열 선택이 나오는데 이걸 눌러주면 

추가적으로 세부항목 내부에서 보고 싶은 내용을 볼 수 있도록 선택 할 수 있다.

여기서 스레드를 선택하고 

확인을 눌러주면 

이렇게 스레드 항목이 추가되고 확인할 수 가 있다.

 

스레드를 알기 위해서는 먼저 프로세스란것을 알아야 한다

 

프로세스(Process)

프로그램이 실행될 때 운영체제로부터 할당받는 독립적인 자원 단위로 메모리, CPU 시간 등을 이야기한다.
하나의 애플리케이션은 하나의 프로세스로 실행된다.

예를 들면 바탕화면에 깔려있는 프로그램을 실행시키면 실행상태가 되고 메모리에 할당이 이뤄면서 이때부터 프로세스가 된다.

 

스레드(Thread)

프로세스 내에서 실행되는 작업의 세부 단위로 여러 스레드는 하나의 프로세스가 할당받은 자원을 공유하며 작업을 수행한다.
예를 들어, 프로세스가 메모리(Code, Data, Heap 등)를 제공하면 스레드가 이를 활용해 작업을 처리한다.

 

이 스레드는 프로세스 안에서 하나 혹은 다수가 존재한다.

이를 싱글 스레드 프로세스, 멀티 스레드 프로세스라고 부르는데 이를 분류해서 설명하자면 아래와 같다

 

싱글 스레드 프로세스

  • 하나의 실행 흐름만 가진다

이는 한번에 하나의 작업만 순차적으로 처리한다는 뜻으로 예를 들어, 웹 브라우저의 메인 스레드에서 HTML 파싱, CSS 적용, JavaScript 실행을 순차적으로 수행하는 것과 같은것을 말한다.

  • 자원 접근 동기화가 불필요하다

이는 단일 스레드이기 때문에 자원 공유에 대한 문제를 발생하지 않는다는 의미로JavaScript에서 변수에 접근할때 동기화 문제를 고려할 필요가 없는 것과 같은것을 말한다

  • 문맥 교환(context switch) 작업이 없다

문맥 교환이란 현재 실행중인 프로세스나 스레드의 상태를 저장하고 다른 프로세스나 스레드로 전환하는 과정을 말하는데 이는 CPU가 여러작업을 번갈아가면서 실행하기 때문에 발생한다. 
CPU안에는 코어라는게 있는데 이 코어는 CPU 내부에 있는 실제 연산을 수행하는 핵심 처리 장치이다.
이 코어는 한번에 하나의 스레드를 처리할 수 있는데, 멀티 스레드 프로세스의 경우는 하나의 코어가 한 프로세스 내부에 있는 모든 스레드를 왔다갔다 하면서 처리하면서 사실상 하나의 코어는 하나의 프로세스를 처리할 수 있도록 보이게 한다.
이 과정에서 스레드 사이를 건너 뛰기 위해서는 이전에 스레드가 어떤 작업을 하고 있었는지에 대한 상태를 저장하고 다른 스레드로 이동하는데 싱글 스레드 프로세스의 경우는 하나의 프로세스 안에서 코어가 문맥교환을 할 필요가 없기 때문에 자원이 많이 필요로 하지 않는다.(물론 동일하게 프로세스의 사이를 왔다갔다 하면서 처리하기도 하는데 프로세스를 왔다 갔다 하는 것을 기본으로 싱글 스레드와 멀티 스레드의 자원의 소모를 보면 멀티가 훨씬 많은 문맥교환을 한다(프로세스 내부에서 스레드도 왔다갔다 하면서 문맥교환을  하기 때문)).

그렇기에 싱글 스레드 프로세스는 오버해드가 감소한다(작업 전환에 따른 추가적인 시간과 자원 소모가 적다)

  • CPU 집약적 작업에 효율적이다

이는 단순하고 연속적인 계산 작업에 적합하다는 의미이다.

  • 여러 CPU 코어를 활용하지 못한다

 

이는 프로세스 내부에 스레드가 하나기 때문에 하나의 코어가 일을 하기 시작하면 스레드 하나는 하나의 코어에 묶이게 된다. 싱글 스레드는 작업을 병렬로 분할 작업할 수 없기 때문에 다른 코어들은 유휴 상태로 남는다는 의미이다.

 

멀티 스레드 프로세스

  • 여러 실행 흐름을 동시에 처리한다

여러 작업을 병렬로 수행할 수 있다란 의미로 웹 서버에 각 클라이언트의 요청을 별도로 스레드로 처리하여 동시에 여러 요청을 처리하는 것과 같다.

  • 자원 공유로 효율성이 증가한다

프로세스 내의 스레드들이 메모리와 자원을 공유한다는 의미로 멀티스레드 그래픽 편집 프로그램에서 여러 스레드가 같은 이미지 데이터에 접근해서 작업하는 것과 같다.

  • 응답성이 높다

하나의 스레드가 블로킹 되어도 다른 스레드가 계속 처리해준다는 것이다. 

  • 자원 접근 동기화가 필요하다

여러 스레드가 공유 자원에 접근할 때 동기화 문제가 발생할 수 있다는 것으로 멀티스레드 뱅킹 시스템에서 동일 계좌에 대한 동시 접근을 뮤텍스와 세마포어를 사용해서 제어해야한다.
예를 들어, 하나의 방에 A와 B라는 룸메이트가 있다(이 두개가 스레드).
이때 둘 다 동시에 콜라를 마실려고하는데

1. A가 냉장고를 열고 콜라가 있는걸 확인하고

2. 동시에 B도 냉장고를 열고 콜라가 있는걸 확인하고

3. A가 콜라를 꺼내 마시고

4. 동시에 B도 콜라를 꺼내려고 하지만 이미 없어졌다.
그렇지만 이 상황에서 B는 콜라가 있다고 생각하고 먹으려 하지만 실제로는 없어져서 혼란스러워 한다.

이게 동기화 문제이다.

 

이를 해결 하기 위해서는 누군가 콜라를 마시려고 혹은 뭔가를 마시하려고 할때 냉장고에 "사용중임"을 띄워 놓으면 (이게 뮤텍스나 세마포어의 역할이다)

 

1. A가 냉장고를 사용할 때 "사용 중" 표시를 한다.
2. B는 이 표시를 보고 기다린다.
3. A가 사용을 마치면 "사용 가능" 표시로 바꾼다.
4. B가 이제 안전하게 냉장고를 열어 우유의 유무를 확인할 수 있다.

 

  • 프로그래밍 난이도가 높다.

동기화, 데드락 등의 복잡한 문제를 고려해야 한다.

예시로 멀티스레드 프로그램에서 스레드 간 데이터 경쟁 조건을 방지하기 위해 세밀한 동기화 로직이 필요하다

더보기

데이터 경쟁 조건(Race Condition)
데이터 경쟁 조건은 여러 스레드가 동시에 같은 데이터에 접근하여 수정할 때 발생하는 문제입니다. 이 경우, 스레드의 실행 순서나 타이밍에 따라 결과가 달라질 수 있습니다. 예를 들어, 두 개의 스레드가 같은 변수를 동시에 증가시키려고 할 때, 각 스레드가 변수의 값을 읽고 수정하는 과정에서 서로의 작업이 겹치면 예상치 못한 결과가 나올 수 있습니다.

 

예를 들어

1. 공유 변수 - counter라는 변수가 있다고 가정해보자.
2. 스레드 A와 B - 두 개의 스레드가 이 counter 변수를 1씩 증가시키려고 한다.
3. 경쟁 조건 발생

  • 스레드 A가 counter의 현재 값을 읽는다 (예: 10).
  • 그 사이에 스레드 B도 같은 값을 읽는다 (여전히 10).
  • 이제 두 스레드는 각자 1을 더하고 결과를 다시 counter에 저장한다.
  • 최종적으로 counter는 11이 됩니다. 하지만 실제로 두 번 증가해야 하므로 올바른 결과는 12여야 한다.

이런 문제를 방지하기 위해 동기화 로직이 필요하다.

동기화는 여러 스레드가 동시에 공유 자원에 접근하지 못하도록 하여 데이터의 일관성을 유지하는 방법으로

  • 뮤텍스(Mutex): 뮤텍스를 사용하면 한 스레드가 공유 자원에 접근할 때 다른 스레드는 대기하게 된다. 즉, 한 스레드가 작업을 완료할 때까지 다른 스레드는 해당 자원에 접근할 수 없다.
  • 세마포어(Semaphore): 세마포어는 특정 수의 스레드만 공유 자원에 접근할 수 있도록 제한하는 메커니즘이다.
  • 임계 영역(Critical Section): 임계 영역은 공유 자원에 접근하는 코드 블록으로, 이 영역은 동시에 여러 스레드에서 실행되어서는 안된다.

와 같은 방법들이 있다.

 

08. Node.js가 비동기 작업을 처리하는 방법

자바스크립트는 싱글 스레드이고 Node.js는 자바스크립트 언어를 사용하는데, Node.js를 사용할때 어떻게 비동기로 파일을 열고 HTTP로 리퀘스트를 보내는 걸까 

 

과정을 천천히 살펴보자면 아래와 같다.

 

1. V8에서 libuv가 필요함

JavaScript는 자체적으로 파일 시스템 작업이나 네트워크 작업 같은 저수준 작업을 수행할 수 없으므로, Node.js는 이러한 작업을 처리하기 위해 C/C++로 구현된 네이티브 코드(libuv)를 사용한다.

JavaScript 코드에서 비동기 작업 요청이 발생하면 Node.js는 이를 libuv에 넘겨준다.

 

2. Node.js가 libuv 코드와 바인딩

Node.js는 **C++ 애드온(바인딩)**을 통해 libuv를 호출할 수 있다.

예를 들어, JavaScript에서 fs.readFile()을 호출하면 내부적으로 Node.js는 libuv에 파일 읽기 작업을 요청한다.

 

3. libuv가 작업을 처리 메커니즘에 전달

작업의 종류에 따라 libuv는 요청을 적합한 메커니즘으로 전달한다

  • Thread Pool: 파일 시스템 작업, DNS 조회 등 CPU를 많이 사용하는 작업은 libuv의 Thread Pool로 전달된다.
  • OS Kernel: 네트워크 작업 등 비차단 I/O는 OS의 이벤트 통지 메커니즘(epoll, kqueue, IOCP 등)을 사용한다.
  • 내부 타이머 시스템: 타이머 작업은 libuv 자체 구조에서 관리된다.

이렇게 libuv가 작업을 처리할 담당자에게 전달하는 과정은 libuv의 내부 구조에 따라 그냥 libuv 그 자체가 판단해서 처리한다고 보면 된다.

 

4. 처리가 완료된 작업의 콜백 함수가 Event Queue에 등록

Thread Pool이나 OS Kernel이 작업을 완료하면, 그 결과는 libuv로 다시 전달된다.

libuv는 해당 작업에 연결된 콜백 함수를 Event Queue에 추가한다.

이때 결과를 전달하는건 Thread Pool과 OS Kernel이 서로 다른 과정을 거치나 결국 Event Loop에 의해 Event Queue에 추가한다( Thread Pool의 워커 스레드 또는 OS Kernel이 작업 완료 신호를 libuv로 전달하면Event Loop가 이를 감지하고, Event Queue에 콜백을 등록한다)

 

5. Event Loop가 Event Queue에서 콜백 함수를 가져와 처리

Event Loop는 Event Queue를 확인하며 완료된 작업의 콜백을 하나씩 가져온다.

이 콜백은 JavaScript 컨텍스트에서 실행할 준비가 된 상태로 전달된다.

 

6. V8 엔진이 콜백 함수를 실행

최종적으로 V8 엔진이 Event Loop로부터 전달받은 콜백 함수를 실행한다.

JavaScript 코드가 실행되고, 콜백 내부에서 또 다른 비동기 작업이 요청될 수 있다.

이 경우, 새로운 작업 요청은 다시 3번 단계부터 시작된다.

 

결국에 Node.js는 싱글 스레드로 프로세스를 처리하는 과정에서 필요한 비동기 작업은 libuv에게 위임하면서 싱글 스레드임에도 불구하도 비동기 작업의 처리가 가능하게 된다.

 

09. Event Loop란

libuv의 이벤트 루프는 Node.js의 비동기 I/O 모델의 중추적인 역할을 하며, 다양한 시스템 API와 스레드 풀을 통해 효율적인 작업 처리를 가능하게 한다.

 

Evnet Loop의 단계

이벤트 루프는 여러 단계로 나뉘며, 각 단계는 특정 유형의 콜백을 처리하는 FIFO 큐를 가지고 있다.

  • Timers

이 단계에서는 setTimeout()과 setInterval()에 의해 예약된 콜백을 실행한다. 타이머가 만료되면 해당 콜백이 큐에 추가되고 실행된다.
예를 들자면, setTimeout(() => console.log("타이머 호출"), 1000); 이 코드는 1초 후에 "타이머 호출"을 출력한다. 그러나 이 단계는 poll 단계가 끝난 후에 실행되므로, 실제로는 1초 후에 실행될 수 있지만 그보다 더 늦게 실행될 수도 있다.

  • Pending Callbacks

이 단계에서는 이전 이벤트 루프 반복에서 지연된 I/O 콜백을 처리한다. 예를 들어, TCP 연결 오류와 같은 특정 상황에서 발생한 콜백이 여기에 포함된다.
예를 들면, 네트워크 요청 중 오류가 발생했을 때, 해당 오류를 처리하는 콜백이 이 단계에서 실행된다.

  • Idle/Prepare

I/O 이벤트를 기다리고 처리하는 단계이다. 

이 단계에서는 파일 읽기, 네트워크 요청 등 대부분의 I/O 관련 콜백이 처리된다.
예를 들면, 클라이언트의 요청이 들어오면 해당 요청을 처리하는 콜백이 여기서 실행된다. 만약 대기 중인 요청이 없다면, 새로운 I/O 요청을 기다리거나 타이머가 만료될 때까지 대기한다.

  • Poll

I/O 이벤트를 기다리고 처리하는 단계이다. 

이 단계에서는 파일 읽기, 네트워크 요청 등 대부분의 I/O 관련 콜백이 처리된다.
예를 들면, 클라이언트의 요청이 들어오면 해당 요청을 처리하는 콜백이 여기서 실행된다. 만약 대기 중인 요청이 없다면, 새로운 I/O 요청을 기다리거나 타이머가 만료될 때까지 대기한다.

  • Check

setImmediate()로 등록된 콜백을 실행하는 단계이다. 이 단계는 poll 단계가 끝난 직후에 실행된다.
예를 들면, setImmediate(() => console.log("즉시 호출")); 이 코드는 poll 단계가 끝난 후 즉시 "즉시 호출"을 출력한다.

  • Close Callbacks

소켓이나 파일과 같은 리소스가 닫힐 때 발생하는 콜백을 처리한다.
예를 들면, 웹소켓 연결이 종료되었을 때, 연결 종료를 알리는 콜백이 여기에 등록되어 실행된다.

 

I/O 콜백의 흐름

I/O 작업이 완료되면 해당 작업의 결과를 처리하기 위해 등록된 콜백 함수가 큐에 추가된다. 

이런 콜백은 주로 poll 단계에서 실행되며, 대기 중인 I/O 이벤트가 있을 경우 즉시 처리된다.
다음 단계로 넘어가는 것을 '틱(tick)'이라고 불린다.

예를 들어, poll 단계에서 모든 대기 중인 I/O 콜백이 처리되면 다음으로 check 단계로 한틱 넘어간다.
만약 어떤 단계에서 처리할 콜백이 없다면 그 단계를 건너뛰고 다음 단계로 바로 넘어간다. 

예를 들어, poll 단계에서 대기 중인 I/O 이벤트가 없으면 즉시 check 단계로 이동하게 된다.

 

이벤트 루프는 싱글 스레드이기 때문에 timers 페이즈에 있는 일이 끝나거나 최대 콜백 수가 될 때 까지 도달 하면 다음 단계(페이즈)로 이동하게 된다.

 

10. Event Loop의 Phase별 구체적 설명

이벤트 루프 내부에서 Timer, Poll, check phase 부분이 중요하기에 조금 더 알아보고자 한다.

 

Timers Phase

이벤트 루프의 타이머 단계는 Node.js의 비동기 처리에서 중요한 역할을 하며, 주로 setTimeout()과 setInterval()로 예약된 콜백을 실행하는 단계이다. 

 

타이머 단계의 동작 원리


1. 타이머 등록
사용자가 setTimeout(fn, delay) 또는 setInterval(fn, delay)를 호출하면, 해당 콜백 함수와 지연 시간(delay)이 큐에 등록된다. 이때, 현재 시간과 함께 타이머가 min-heap 구조에 저장된다. min-heap은 가장 빠른 타이머를 효율적으로 찾기 위한 자료구조로, 타이머가 오름차순으로 정렬된다.


2. 타이머 단계 진입
이벤트 루프가 타이머 단계에 진입하면, 현재 시간(now)과 각 타이머의 등록 시간(registeredTime), 지연 시간(delay)을 비교하여 콜백을 실행할 수 있는지 확인한다.

 

3. 콜백 실행 조건
타이머의 조건은 다음과 같다: 

 

\(now − registeredTime ≥ delay \)

 

이 조건이 참이면 해당 콜백을 실행하고, 그렇지 않으면 다음 타이머를 검사한다.

만약 모든 타이머가 아직 실행할 수 없는 상태라면, 다음 단계로 넘어간다.

 

4. 타이머 실행 예

setTimeout(() => {
    console.log("타이머 A");
}, 1000);

setTimeout(() => {
    console.log("타이머 B");
}, 500);

 

위 코드를 실행하면, 500ms 후에 "타이머 B"가 출력되고, 1000ms 후에 "타이머 A"가 출력된다. 이벤트 루프는 먼저 500ms가 지난 타이머를 확인하고 콜백을 실행한 후, 그 다음 1000ms가 지난 타이머를 확인하여 실행한다.

 

5. 실행 순서

모든 타이머를 검사한 후, 해당 콜백들을 실행하거나 시스템의 실행 한도에 도달하면 다음 단계인 Pending Callbacks로 넘어간다. 이때, 하나 이상의 타이머가 준비되어 있으면 이벤트 루프는 다시 타이머 단계로 돌아가서 해당 콜백들을 실행한다.

 

6. 정확한 실행 보장
중요한 점은 setTimeout(fn, delay)로 설정한 지연 시간이 정확히 그 시간 후에 콜백이 실행된다는 보장이 없다는 것이다. 예를 들어, 1000ms 후에 실행되도록 설정했더라도, 이전 단계에서 블로킹 작업이나 다른 콜백들이 남아 있다면 실제로는 그보다 더 늦게 실행될 수 있다.


7. 큐 비우기
만약 큐에 남아있는 모든 타이머가 처리되면, 이벤트 루프는 다음 단계로 넘어간다. 이때 대기 중인 I/O 작업이나 다른 콜백들이 있는지 확인하게 된다.

**Timers Phase가 강제적으로 실행되는 단계는 아니며, setTimeout이나 setInterval을 코드에서 사용하지 않으면 이 단계는 실제로 아무 일도 일어나지 않고 넘겨버릴 수 있다

 

쉽게 예를 들어 설명하면, Timer에서 setTimeout이 20초를 파라미터로 받아서 전달이 오면 해당 구조를 Timer의 Queue에 넣는게 아니라 먼저 min-heap이란 곳에 넣어준다.

그리고 이 공간에 넣어줄때 대기 시간을 확인한 후에 우선순위를 결정한 후 에 순서를 맞춰 타이머를 넣어준다.

그리고 이 타이머의 시간을 대기하고 있게 하다 20초가 모두 지나면(지정된 시간이 모두 지나면) 꺼내서 Timer Queue에 넣어주고 시간이 다 된 타이머 콜백이 바로 실행 될 수 있도록 한다.

 

이렇게 min-heap공간을 따로 둔 이유는 타이머가 여러개 있을때 어떤걸 가장 먼저 실행해야 할 지 모르기 때문이다.

min-heap공간에 두면 최소값이 항상 루트에 위치하게 되어 있기에 타이머를 효율적으로 정렬하고 가장 먼저 실행해야할 타이머 콜백을 빠르게 꺼내 처리할 수 있게 된다.

 

그리고 타이머의 작업의 완료는 타이머 페이즈가 아닌 poll phase에서 결정한다.

위에서 말했다 싶이 만약에 poll phase에서 만약 11초가 걸리는 작업이 먼저 실행되고 setTimeout에 10초가 걸려 있는 작업이 있다.

먼저 코드를 모두 읽으면서 setTimeout이 min-heap에 저장된 후에 poll phase를 시작한다.

Event Loop는 싱글 스레드로 구성되어 있기 때문에 poll phase가 종료되지 않으면 setTimeout의 시간이 되었다고 해도 setTimeout을 시작하지 않고 poll phase가 종료되어야만 setTimeout이 시작된다 그래서 setTimeout으로 작성된 콜백은 11초 이후부터 실행하기 시작한다.

이게 위에서 말한 정확한 시간 보장에 대한 내용이다.

 

Poll Phase의 동작 방식

 

1. I/O 콜백 처리

Poll Phase의 가장 중요한 기능은 I/O 작업을 처리하는 것이다.

여기서 I/O 작업이란, 네트워크 요청, 파일 시스템 작업, 데이터베이스 쿼리 등과 같은 비동기적인 작업을 말한다.

이벤트 루프는 I/O 작업이 완료되었을 때, 해당 작업에 등록된 콜백을 실행한다.

예를 들어, HTTP 요청을 처리하기 위해 비동기적으로 데이터를 읽은 뒤, 그에 대한 처리가 필요하면 콜백을 실행한다.

 

2. 대기 중인 콜백 확인

Poll Phase는 두 가지 주요 작업을 수행한다.

  • I/O 콜백을 처리 - I/O가 완료된 콜백을 확인하고 처리한다.

3. 타이머 콜백과 I/O 콜백의 우선순위

타이머 콜백과 I/O 콜백이 모두 대기 중이라면, I/O 콜백이 우선적으로 처리된다.

이는 실제로 I/O 작업의 처리가 중요하기 때문이다.

타이머 콜백은 설정된 시간 이상이 경과했지만, Poll Phase에서 I/O가 먼저 처리되면 그 이후에 실행될 수 있다.

 

4. 타임아웃과 콜백 실행

타이머가 만료된 경우, 만약 Poll Phase에서 대기 중인 타이머 콜백이 있다면, 이를 처리하기 시작한다.

이때 타이머 콜백이 실행되기 전에 우선적으로 I/O 작업이 완료된 콜백을 처리하고, 그 후에 타이머 콜백을 처리한다.

Poll Phase에서는 I/O 작업을 완료하고 남은 시간에 타이머 콜백을 실행하는 방식으로 동작한다.

 

5. 새로운 I/O 작업 대기

Poll Phase는 주로 I/O 작업을 처리하는 동안 새로운 I/O 요청이 있을 경우, 이 요청을 대기 큐에 넣고 계속해서 I/O를 기다린다. 이벤트 루프가 반복되면서 새로운 I/O 작업이 들어오면 계속해서 이를 처리할 수 있다.

 

Poll Phase의 특징

  • 블로킹 모드와 논블로킹 모드

Poll Phase에서는 블로킹 모드로 동작할 수 있다. 즉, I/O 작업이 모두 처리될 때까지 이벤트 루프가 블록될 수 있다. 그러나 논블로킹 모드에서는 I/O 작업이 완료될 때까지 기다리지 않고, 다른 작업을 계속해서 처리할 수 있다. 이 두 가지 모드는 I/O 처리 방식에 따라 달라지며, Node.js에서는 논블로킹 모드를 선호한다.

 

  • 콜백이 대기하는 상태

Poll Phase에서는 I/O 콜백이 대기 상태로 대기할 수 있는데, 이는 I/O 작업이 완료되기 전에 콜백을 실행할 수 없기 때문이다. I/O 콜백이 실행될 때까지 이벤트 루프는 대기 모드로 남아 있을 수 있다.

 

  • 우선순위에 따른 실행

Poll Phase에서 처리하는 주요 작업은 I/O 콜백과 타이머 콜백이다. 이때 I/O 콜백이 우선적으로 처리되며, 타이머 콜백은 I/O 콜백이 모두 처리된 후 실행된다. 이 때문에 타이머 콜백의 실행 시점이 예상보다 지연될 수 있다.

 

 

Poll Phase는 I/O 작업과 타이머 콜백을 처리하는 중요한 단계다.

I/O 작업이 완료되면 해당 콜백을 실행하고, 타이머 콜백은 I/O 작업이 끝난 후 처리된다.

I/O 콜백이 우선순위가 높고, 그 이후에 타이머 콜백이 처리된다.

Poll Phase는 I/O 작업을 블로킹 또는 논블로킹 모드로 처리하며, I/O 요청이 있을 경우 계속해서 대기하고 실행한다.

 

이 Poll phase가 끝나면 check phase가 실행되는데 이때 setImmediate로 예약된 콜백 함수들이 check phase로 넘어가서 실행되게 된다.

 

우선 순위는 IO > Timer > setImmediate 라고 보면 된다.

 

11. setImmediate &  setTimeout & process.NextTick

setTimeout, setInterval은 Timers 단계에서 처리가 되고 setImmediate는 Check 단계에서 처리가 된다.

그리고 process.nextTick은 이벤트 루프 시작 시와 이벤트 루프 각 단계에서 처리하게 된다.

 

process.nextTick은 조금 특별한 API이기 때문에 좀 자세하게 확인해보자.

 

process.nextTick()은 Node.js에서 콜백을 즉시 실행하도록 예약하는 특별한 메소드이다. 이를 사용하면, 현재 실행 중인 이벤트 루프의 현재 단계가 끝난 후 바로 콜백을 실행할 수 있다. process.nextTick()은 매우 높은 우선순위를 가진 메소드로, 이벤트 루프에서 다른 작업들이 대기 중이라 하더라도 가장 먼저 실행된다.

 

process.nextTick()의 동작 원리

  1. 즉시 실행 예약:
    • process.nextTick()은 콜백을 다음 이벤트 루프 사이클이 시작되기 전에 즉시 실행되도록 예약한다.
    • 즉, process.nextTick()으로 예약한 콜백은 현재 실행 중인 코드 블록이 완료된 후, 다음 단계로 넘어가기 전에 실행된다.
  2. 우선순위:
    • process.nextTick()으로 예약된 콜백은 모든 타이머 콜백, I/O 콜백, setImmediate 콜백보다 우선하여 실행된다.
    • 이 때문에, process.nextTick()은 다른 비동기 작업들보다 먼저 실행되며, 이벤트 루프가 다른 작업들을 처리하는 것보다 먼저 실행되므로 Poll Phase나 Check Phase로 넘어가기 전에 실행된다.
console.log("Start");

process.nextTick(() => {
  console.log("Executed after the current event loop cycle");
});

console.log("End");

이렇게 입력한 코드의 결과는

Start
End
Executed after the current event loop cycle

이 된다.

 

먼저 코드를 한번읽어야 하기 때문에 console.log() 로 되어 있는 코드는 바로 실행된다.

그리고 전 코드를 모두 일고 난 후에 process.nextTick이 실행된다.

즉, process.nextTick()의 콜백은 현재 실행 중인 코드가 끝난 직후 실행되며, 그 어떤 다른 비동기 작업보다 먼저 실행된다.

중요성

  • 우선순위가 높은 콜백: process.nextTick()은 콜백을 가장 우선적으로 실행하게 하여, 이벤트 루프가 다른 작업들(예: 타이머나 I/O 작업)을 처리하기 전에 중요한 작업을 먼저 처리할 수 있게 한다.
  • 콜백의 순서 보장: 여러 process.nextTick() 호출이 있을 경우, 그 콜백들은 FIFO(First In, First Out) 순서대로 실행된다. 즉, 먼저 등록된 process.nextTick() 콜백이 먼저 실행된다.

사용할 때 주의점

  1. 과도한 사용은 이벤트 루프 차단:
    • process.nextTick()은 이벤트 루프를 차단할 수 있다. process.nextTick()으로 너무 많은 콜백을 예약하면, 이벤트 루프가 계속해서 nextTick 콜백만 처리하게 되어 다른 I/O 작업이나 타이머 콜백을 처리할 기회를 잃게 된다.
    • 이로 인해 블로킹 상태가 발생할 수 있으며, I/O 지연이나 응답 지연이 발생할 수 있다
let count = 0;
function recursiveNextTick() {
  if (count < 100) {
    process.nextTick(recursiveNextTick);
    count++;
  } else {
    console.log("Done");
  }
}

recursiveNextTick();

이 코드에서는 process.nextTick()으로 계속해서 콜백을 예약하고 있기 때문에, console.log("Done")은 이벤트 루프가 다른 작업을 처리할 기회를 가지지 못하고 매우 느리게 실행될 수 있다.

 

이벤트 루프 차단을 피하려면:

  • process.nextTick() 대신 setImmediate()나 setTimeout()을 사용하여 이벤트 루프를 차단하지 않도록 해야 한다. 이 두 메소드는 비슷하지만 process.nextTick()은 즉시 실행되는 반면, setImmediate()와 setTimeout()은 약간의 지연을 두고 실행된다.

우선 순위를 보자면 process.nextTick > setImmediate > setTimeout  인데 사실 setImmediate - setTimeout은 프로세스의 성능에 의해 제한되기에 비결정적이다. 그냥 랜덤이라고 생각하면 된다고 함.

 

그런데 하나 생각해야할 것은 이벤트 루프에 단계에 따라서 이 순서가 달라질 수 도 있다는 점이다

예를 들어서 

const fs = require('fs');

console.log("Start");

// 파일 읽기 작업 (I/O 작업)
fs.readFile(__filename, () => {
  console.log("File read complete");
  
  // setTimeout(0)
    setTimeout(() => {
      console.log("setTimeout callback");
    }, 0);

    // setImmediate()
    setImmediate(() => {
      console.log("setImmediate callback");
    });
});



console.log("End");

 

이런 코드가 있다고 할때 가장 먼저 파일 읽기가 실행된다.

파일 읽기 작업이 시작되면, Node.js는 이 작업을 비동기적으로 처리하기 위해 libuv를 통해 OS에 요청을 보낸다. 이 요청은 별도의 스레드에서 처리되며, I/O 작업이 완료되면 콜백이 호출된다.

그리고 IO 작업이 완료된 후 콜백이 실행되는데 이 콜백 안에서 setTimeout()과 setImmediate()가 호출된다.

그리고 나서 타이머가 등록 되는데, setTimeout(() => { console.log('timeout'); }, 0);은 타이머 큐에 추가된다. 하지만 실제로는 최소 지연 시간이 1ms로 설정되므로, 이 콜백은 타이머 단계에서 다음으로 실행될 수 있다.

I/O 작업이 완료된 후 이벤트 루프는 check phase로 이동한다. 이 단계에서는 setImmediate로 등록된 콜백이 실행된다.

그 다음, 이벤트 루프는 timers phase로 이동한다. 이 단계에서는 타이머 큐에 등록된 콜백을 확인하고, 만약 시간이 경과했다면 해당 콜백을 실행한다.

 

그래서 출력되는 순서는

1. I/O 작업 완료 후 콜백 실행
2. Check Phase에서 setImmediate() 실행
3. Timers Phase에서 setTimeout() 실행

이 된다.

 

그래서 그냥 우선순위가 중요한게 아니라 이벤트 루프의 단계에 대해서도 생각해야만 한다.

 

여기 부분은 정확하지 않은 정보가 많음.... 사실상 명확한게 뭔지 알 수가 없다.

 

그냥 알아 둘건 IO가 제일 먼저 처리되는데 그 IO 안에 timers phase에 있는 들어갈 함수와 immediate가 같이있으면 구조상 무조건 immediate가 먼저 실행되고 다음에 timer phase로 다시 돌아가서 timers 함수가 실행된다는 것이다.

 

이는 IO작업이 있기에 가장 먼저 처리하려고 하고 그러면 Immediate가 있기에 check phase에서 처리하고 난 후에 다시 timers phase로 갔을때 setTimeout이 처리된다는 것이다.

 

이게 그냥 핵심적인 내용으로 보임...

 

 

12. Node.js Event Emitter

 

자바스크립트에서 브라우저에서 이벤트가 발생했을때 어떻게 반응할 지에 대해서 등록을 하는 코드를 작성했던 기억이 있을 것이다.

Node.js에서도 이와 같은 방식으로 코드를 작성하는 방법이 있는데 그때 사용하는 것이 Event Emitter 이다.

Node.js의 EventEmitter는 이벤트 기반 프로그래밍을 구현하기 위한 핵심 메커니즘이다.

이벤트 기반 프로그래밍의 핵심은 "무언가 일어나면, 그에 맞는 행동을 한다"는 것이다.

EventEmitter는 옵저버 패턴의 구현체이다.

옵저버 패턴은 객체의 상태 변화를 관찰하는 관찰자들을 객체에 등록하여, 상태 변화가 있을 때마다 메서드 등을 통해 객체가 직접 관찰자들에게 통지하도록 하는 디자인 패턴이다.

 

자바를 사용해서 옵저버 패턴을 구현하자면 아래와 같다.

import java.util.ArrayList;
import java.util.List;

// Observer 인터페이스
interface NewsObserver {
    void update(String news);
}

// Subject 클래스
class NewsPublisher {
    private List<NewsObserver> subscribers = new ArrayList<>();
    private String latestNews;

    public void subscribe(NewsObserver observer) {
        subscribers.add(observer);
    }

    public void unsubscribe(NewsObserver observer) {
        subscribers.remove(observer);
    }

    private void notifySubscribers() {
        for (NewsObserver subscriber : subscribers) {
            subscriber.update(latestNews);
        }
    }

    public void publishNews(String news) {
        this.latestNews = news;
        notifySubscribers();
    }
}

// Concrete Observer 클래스
class NewsSubscriber implements NewsObserver {
    private String name;

    public NewsSubscriber(String name) {
        this.name = name;
    }

    @Override
    public void update(String news) {
        System.out.println(name + "님, 새로운 뉴스: " + news);
    }
}

// 메인 클래스
public class ObserverPatternDemo {
    public static void main(String[] args) {
        NewsPublisher publisher = new NewsPublisher();

        NewsSubscriber subscriber1 = new NewsSubscriber("홍길동");
        NewsSubscriber subscriber2 = new NewsSubscriber("김철수");

        publisher.subscribe(subscriber1);
        publisher.subscribe(subscriber2);

        publisher.publishNews("코로나19 백신 개발 성공!");

        publisher.unsubscribe(subscriber2);

        publisher.publishNews("새로운 AI 기술 발표!");
    }
}

 

이렇게 구현하면 발행자를 구독하는 구독자들은 원할때 이 발행자를 구독할 수 있고, 발행자는 누가 구독하든지 상관없이(구독자들이 어떻게 구현되는지는 상관없이) 일괄적으로 어떤 새로운 정보를 알릴 수 있다.

 

Node.js도 Event 모듈을 사용해서 유사한 시스템을 구축할 수 있는 옵션을 제공한다.

이 모듈은 이벤트를 처리할 수 있는 Evnet Emmiter 클래스를 제공해준다.

// events 모듈에서 EventEmitter 클래스를 가져온다.
const EventEmitter = require('events');

// EventEmitter의 인스턴스 생성
const myEmitter = new EventEmitter();

// 'event'라는 이름의 이벤트를 구독
myEmitter.on('event', () => {
    console.log('이벤트가 발생했습니다!');
});

// 'event'라는 이름의 이벤트를 발생
myEmitter.emit('event');

이렇게 확인해볼 수 있다.

여기서 on은 이벤트가 발생했을때 어떤 행위를 할지를 등록하는 부분으로 콜백 함수의 내부에 작성하면 된다.

emit은 트리거의 역할을 하는 것으로 전달된 문자열은 on에서 첫번째 인자에 맵핑되어 emit 호출시에 해당 문자열에 맞게 on으로 등록한 콜백 함수를 실행해준다.

여기서 emit에 두번째 전달인자를 전달해주면 on으로 등록하는 콜백함수에 인자로 받아올 수 가 있다.

myEmitter.on('event', (test) => {
    console.log('전달된 인자는:', test);
});

myEmitter.emit('event', "test");

 

node.js에서는 이 Event Emmiter를 사용해서, 옵저버 패턴을 만들어 사용할 것이다.