Node.js Reactor Pattern

김대현·2021년 4월 1일
1

Node.js 디자인 패턴

목록 보기
1/1
post-thumbnail

Node.js 디자인 패턴 책을 읽으며 들은 생각과 정리를 적은 글입니다.

블록킹 I/O와 논 블록킹 I/O

I/O는 컴퓨터가 동작하는 시간중에서 매우 느린 축에 속하고, 사용자의 입력에 걸리는 시간까지 포함한다면, 더더욱 느린 작업이다. 디스크 엑세스의 경우 몇 ms에서, 사용자의 입력 대기는 예측할 수 없는 시간이 걸린다.

블로킹 I/O 에서는, I/O 요청이 발생하고 완료할 때 까지의 모든 쓰레드의 실행이 차단된다. 만일 서버 단에서 블로킹 I/O를 사용하여 구현된 웹 서버에서 하나의 스레드에서 I/O 작업이 진행되고 있다면, 개발자는 새로운 스레드를, 혹은 다른 대기 쓰레드를 재사용하여 다른 작업을 처리하는 것이 일반적이다.

이러한 방식은 쓰레드가 I/O를 처리하기 위해서 차단되고, 새로운 쓰레드를 할당하는 자원이 필요하다. 또한 이러한 쓰레딩 작업에서 컨텍스트 교환 작업또한 필요하다.

하지만 논 블록킹 I/O라는 개념이 도입되면서, 시스템 호출은 데이터가 I/O하는 동안 유휴 상태에 머물러 있지 않고, 즉시 반환된다. 그렇다면, 어떻게 실제 데이터가 반환될 때 까지 어떻게 I/O에 접근하여 이 상태를 알 수 있는 걸까?

폴링

기본적인 방법으로, 폴링을 사용하는 방법이 있다. 계속해서 반환할 수 있는 I/O 요청의 결과가 있는지 확인하는 방법이다. 하지만 폴링 방식은 "계속해서" 확인하기 때문에, CPU 자원이 리소스 확인을 위해서 사용된다는 점에서 낭비를 초래한다.

이벤트 디멀티플렉싱

앞서 다룬, 폴링 방식은 최신 운영체제에는 적합하지 않은 단점을 갖고있기에 사용되지 않는다. 따라서 동기 이벤트 디멀티플렉서 혹은 이벤트 통지 인터페이스 방식이 사용된다.

자바스크립트에서 다루는 콜 스택과 이벤트 루프가 작동하는 방식을 떠올리면 될 것 같다.

해당 컨셉은 I/O 이벤트 들을 수집하여 큐에 넣고, 처리할 수 있을 때 까지 차단하는 방식이다. 수도코드로 해당 작동방식을 표현해 보면 다음과 같다.

socket A, pipe B;
watchList.add(socketA, "읽기");

while(events = demultiplexer.watch(watchList)){
	for(event in events){
    	data = event.read();
        if(data === "리소스 읽을 필요 없음"){
        	demultiplexer.unwatch(event)
        }
        else{
        	consume(data)
        }
    }

}

코드에서 socketA로 들어온 리소스들은, watchlist에 담기게 된다. 그리고 watchlist에 할당된 리소스들은 각각 demultiplexer로 watch를 하게 된다.

감시되고 있는 리소스들은 동기적으로 읽을 준비가 될 때 까지 다른 작업들은 차단된다. 만일 읽을 수 있다면, demultiplexer는 이를 처리할 것이다. 처리가 되는 와중에도, I/O의 작업은 계속된다.

윗 단락을 계속 반복한다.

이런 방식을 통해서, 하나의 스레드에서는 I/O가 끝난 작업들을 최소한의 유후시간으로 처리할 수 있게되고, 보다 직관적인 해결방법을 가질 수 있다.

  • 다중 IO 작업을 동시에 실행
  • 하나의 스레드로 작업을 처리하는 컨셉은 여러 스레드간의 동기 작업에 대한 필요성을 없앤다.

Node.js에서 사용되는 Reactor Pattern (demultiplexer)


출처: https://cek.io/blog/2015/12/03/event-loop/

gif를 통해서 V8엔진을 지닌 웹 브라우저 또한 이벤트 디멀티플렉서의 개념을 사용하여 Web Api를 처리하는 것을 확인할 수 있다. 즉, Node.js 또한 이 컨셉을 갖고 있으며, 이를 이해하는 것은 Node.js의 런타임을 보다 자세히 이해할 수 있다는 것을 뜻한다.

Reactor Pattern이란, 이벤트 디멀티플렉서의 알고리즘이 적합한 디자인 패턴이다. 핵심적인 개념은 각각의 I/O 이벤트에 핸들러들이 하나 씩 존재한다는 것이고, 이 핸들러들은 각각 I/O가 끝나고, 이벤트 루프에 의해서 처리되는 즉시에 호출된다는 것이다.

해당 gif를 이해하면서 보도록 하자.

setTimout(function cb(){
  console.log("There") // 앞서 말한 핸들러
}, 2000) // 타임아웃은 타이머로써, IO 작업을 할 것이다.

해당 함수가 실행되는 컨텍스트에서, 비동기의 개념을 제외하고 타임아웃 함수가 어떻게 작동하는지 지켜보면 다음과 같은 흐름을 갖는다.

  1. setTimeout 호출

  2. 이벤트 디멀티플렉서(여기선 Web API가) IO 작업을 생성, function cb는 핸들러로 지정된다.

  3. 디멀티 플렉서는 IO 작업이 완료(2초 대기가 완료)된 시점에 이를 콜백 큐(이벤트 큐)에 집어넣는다.

  4. 이벤트 루프는 큐에 쌓인 작업들을 토대로, 지정된 핸들러를 호출한다.

  5. 이벤트 루프가 빌때 까지 4번의 과정을 진행한다.

  6. 이벤트 큐가 비어 졌다면, 이벤트 루프는 차단된다.


해당 패턴을 통해서, JS 개발에서의 비동기 런타임에서의 콜백이 보장하는 효율에 대해서 어느정도 이해할 수 있었다. 또한 경량화된 모듈들이 이루는 함수형 프로그래밍이 가능한 이유도, 앞서 언급한 폴링 방식의 처리방식이었다면 추상화 방식이 굉장히 어려워졌을 것으로 생각이 든다.

profile
블로그 이전 중입니다~

0개의 댓글