[도서] Node.js 디자인패턴 - Chapter 01 - 3

Node.js 디자인패턴을 읽으며 요약/정리한 내용입니다.

Chapter 01) Node.js 플랫폼에 대하여

개요

  • Node.js의 철학 "Node way"
  • Node.js 버전 6와 ES2015
  • Reactor 패턴 - Node.js 비동기 아키텍처의 핵심 매커니즘

1.3 Reactor 패턴

TL;DR

  • 비동기 특성의 핵심인 Reactor 패턴을 분석
  • 단일 스레드 아키텍쳐, 논 블로킹 I/O와 같은 패턴의 기본 개념 확인
  • Node.js 플랫폼 전체에 대한 기반을 형성하는 방법
  • 샘플 코드는 대부분 의사코드로 작성됨

1.3.1 I/O는 속도가 느리다.

  • 기본적으로 입/출력은 컴퓨터의 기본적인 동작 중에서 가장 느림

1.3.2 블로킹 I/O

  • 함수 호출은 작업이 완료될 때 까지 스레드의 실행이 차단
// 데이터를 사용할 수 있을 때까지 스레드가 블록됨
data = socket.read();
// 데이터 사용 가능
print(data);

즉, 함수가 호출되면 해당 함수의 작업이 종료될 때 까지 다른 작업을 진행할 수 없음

  • 웹 서버에서 동시성을 처리하기 위한 전통적인 접근방식: 멀티 스레드, 멀티 프로세스, 스레드 풀 사용
  • 스레드는 시스템 리소스 측면에서 비용이 비싼 방법
    • 메모리 소모, 컨텍스트 전환 등의 비용

1.3.3 논 블로킹 I/O

  • 논 블로킹 I/O: 대부분의 최신 운영체제에서 지원하는 리소스를 액세스하는 매커니즘
    • 해당 운영 모드에서 시스템 호출은 항상 즉시 반환
    • 호출하는 순간에 결과를 사용할 수 없으면, 미리 정의된 상수를 반환하여 그 순간에 반환할 수 있는 데이터가 없음을 나타냄
  • 논 블로킹 I/O에 엑세스하는 패턴
    • busy-waiting: 루프 내에서 리소스를 적극적으로 폴링(polling)하는 것
      • 데이터를 엑세스할 수 있는지 계속 물어보는(확인하는) 것
      • 대부분 엄청난 양의 CPU 시간 낭비를 초래하는 패턴
resources = [socketA, socketB, pipeA];
while(!resources.isEmpty()) {
  for(i = 0; i < resources.length; i++) {
    resource = resources[i];
    let data = resource.read(); // 읽기 시도 -> 폴링하는 부분
    if (data === NO_DATA_AVAILABLE) continue; // 데이터 없음
    if (data === RESOURCE_CLOSED) resources.remove(i); // 리소스 닫힘
    else consumeData(data); // 데이터 사용
  }
}

1.3.4 이벤트 디멀티플렉싱

  • 동기 이벤트 디멀티플렉서 또는 이벤트 통지 인터페이스
    • 최신 운영체제가 제공하는 효율적인 논 블로킹 리소스 처리를 위한 기본적인 매커니즘
    • 감시된 일련의 리소스들로부터 I/O 이벤트를 수집하여 큐에 넣고 처리할 수 있는 새 이벤트가 있을 때까지 차단
socketA, pipeB;
watchedList.add(socketA, FOR_READ); // [1]
watchedList.add(pipeB, FOR_READ);
while(events = demultiplexer.watch(watchedList)) { //[2]
  // 이벤트 루프
  foreach(event in events) { // [3]
    data = event.resource.read(); // read는 블록되지 않고, 비어있어도 항상 데이터를 반환
    if(data === RESOURCE_CLOSED)
      demultiplexer.unwatch(event.resource); // 리소스 닫힘 처리
    else
      consumeData(data); // 데이터 처리
  }
}
  • 의사코드 해석
    1. 리소스를 데이터 구조(List)에 추가
    2. 이벤트 통지자에 감시할 리소스 그룹을 설정
      • 해당 호출은 동기식, 감시 대상 자원 중 하나라도 읽을 준비가 될 때까지 차단
    3. 이벤트 디멀티플렉서에 의해 반환된 각 이벤트 처리
      • 이 시점에서 각 이벤트와 관련된 리소스는 읽을 준비가 되어 있고, 차단되지 않는 상황이 보증
      • 이벤트가 처리되고 나면, 다시 디멀티플렉서에서 처리 가능한 이벤트 발생까지 차단
      • 이를 이벤트 루프(Event Loop)라고 함

  • 하나의 스레드만 사용해도 다중 I/O 사용 작업을 동시에 실행 할 수 있는 능력을 손상시키지 않음
  • 스레드가 아닌 시간에 따른 분산
  • 이를 통해 프로세스 경쟁과 스레드 동기화 걱정이 없는 간단한 동시성 전략을 사용할 수 있음

1.3.5 Reactor 패턴 소개

  • Reactor 패턴: 1.3.4에서 제시된 알고리즘에 특수화된 패턴
  • 핵심 개념: 각 I/O 작업과 관련된 핸들러를 갖는 것, 이 핸들러는 이벤트가 생성되어 이벤트 루프에 의해 처리되는 즉시 호출

    핸들러 => 특정 이벤트가 발생했을 때 실행될 함수 또는 기능, Node.js에서는 callback 함수로 표시됨

  • Reactor 패턴을 사용하는 어플리케이션에서 발생하는 작업 도식
    1. 어플리케이션이 이벤트 디멀티플렉서에 요청을 전달하여 새로운 I/O 작업을 생성
      • 처리가 완료될 때 호출될 핸들러도 지정
      • 해당 작업은 논 블로킹 호출, 즉시 어플리케이션으로 제어 반환
    2. I/O 작업이 완료되면 이벤트 디멀티플렉서는 새 이벤트를 이벤트 큐에 추가
    3. 이벤트 루프이벤트 큐의 항목들에 대해 반복
    4. 각 이벤트에 대해 관련 핸들러가 호출됨
    5. 핸들러는 실행이 완료되면 이벤트 루프에 제어를 반환(5a), 그러나 실행 중 새로운 비동기 동작이 요청(5b)이 발생하면 이벤트 루프로 제어를 반환하기 전에 이벤트 디멀티플렉서에 추가될 수 있음
    6. 이벤트 큐 내의 모든 항목이 처리되면, 루프는 이벤트 디멀티플렉서에서 다시 블록, 처리 가능한 이벤트 생성 시 해당 과정이 트리거

Node.js 어플리케이션은 이벤트 디멀티플렉서에 더 이상 보류 중인 작업이 없고, 이벤트 큐에서 더 이상 처리할 이벤트가 없을 때 자동으로 종료

1.3.6 Node.js의 논 블로킹 엔진 libuv

  • 각 운영체제에는 이벤트 디멀티플렉서에 대한 자체 인터페이스가 있음
    • Linux=epoll, MacOS=kqueue, Windows=I/O Completion Port(IOCP) 등
  • libuv: Node.js 코어 팀이 각 운영체제의 다른 특성으로 인해 이벤트 디멀티플렉서에 대한 높은 추상화가 요구되고, 이를 C 라이브러리로 구현
    • 모든 주요 플랫폼과 호환, 서로 다른 유형의 리소스들의 동작을 표준화
    • 오늘날 libuv는 Node.js의 하위 수준의 I/O 엔진을 말함

1.3.7 Node.js를 위한 구조

  • 리액터 패턴과 libuv가 Node.js의 기본 구성 요소
  • 전체 플랫폼을 위해서는 다음 세가지 구성 요소가 필요
    1. 바인딩 세트: libuv와 기타 낮은 수준의 기능을 JavaScript에 랩핑하고 사용 가능하게 해줌
    2. V8: 크롬 브라우저 용으로 개발한 JavaScript 엔진
    3. 코어 JavaScript API: 상위 수준의 Node.js API를 구현, 노드 코어라고도 함

좋은 웹페이지 즐겨찾기