본문 바로가기

오픈소스/노드

[Node] 01. Node.js 플랫폼

 본 문서는 Node.js 디자인 패턴 바이블을 읽고 리뷰를 남기고 있습니다. 문고들은 이 책의 일부분을 인용한 것임을 밝힙니다.

 

 

# Contents


  • Node.js 철학
  • Node.js 는 어떻게 작동하는가
  • Node.js에서의 JavaScript

 

 

# Node.js 철학


 모든 프로그래밍 플랫폼은 자신들만의 철학, 커뮤니티에서 따르게 되는 일련의 원칙들과 지침, 플랫폼의 진화와 애플리케이션 개발 및 디자인에 영향을 주는 이데올로기를 가지고 있습니다. 이러한 원칙들 중 몇가지는 기술 자체에서 발생하고, 일부는 그것의 생태계에 의해서 발생하고, 일부는 커뮤니티 내에서의 트렌드이며, 일부는 다른 플랫폼에 있던 이데올로기의 진화에 의해서 발생한 것입니다. Node.js에서는 이러한 원칙들 중 일부가 제작자인 Ryan Dahl에 의해서 직접 만들어졌으며, 일부는 코어모듈에 기여한 사람들, 커뮤니티에서의 카리스마 있는 인물들, JavaScript 추세로부터 생겨났습니다.

 

1. 경량 코어

  Node.js 코어는 몇가지 원칙들을 기반으로 자신의 기초를 구성하였습니다. 그 중 한가지는 최소한의 기능 세트를 가지고 코어의 바깥부분에 유저랜드 혹은 유저스페이스라 불리는 사용자 전용 모듈 생태계를 두는 것입니다. 그 영향으로 안정된 테스트와 관리를 통해 생태계 발전에 기여하였습니다.

 

2. 경량 모듈

 작은 것이 아름답다, 각 프로그램은 한 가지 역할만 잘 하도록 만들어라 철학을 끌어올려 패키지 관리자의 도움을 받아 각 패키지가 자신이 필요로 하는 버전의 종속성 패키지들을 갖도록 함으로써 종속성 지옥에서 벗어나게 해줬습니다. 그로 인하여 재사용성 측면을 향상시켜주고, 유지보수가 쉬워지는 장점이 생겼습니다.

 

3. 작은 외부 인터페이스

 Node.js의 모듈들이 갖는 장점은 작은 사이즈와 작은 범위 그리고 최소한의 기능 노출입니다. 이러한 것들이 명확하게 사용될 수 있고, 잘못된 사용에 덜 노출되도록 하는 API 생산 효과를 갖습니다. 대부분의 경우 컴포넌트 사용자는 기능의 확장을 필요로 하거나 부가적인 고급 기능들의 활용 없이 제한되고 집중화된 기능에만 관심이 있습니다.

 

4. 간결함과 실용주의

 완벽하고 모든 기능을 갖춘 소프트웨어와는 반대로 단순하게 설계하는 것이 좋은 실천입니다. 구현을 위해서 적은 노력이 들고, 가볍고 빨리 보급이 가능하며, 유지보수가 쉽고 빠른 이해가 가능한 것이 커뮤니티의 기여를 보다 용이하게 하고, 소프트웨어 자체의 성장과 향상을 돕습니다.

 

 

# Node.js는 어떻게 작동하는가


 Node.js가 내부적으로 어떻게 동작하는지 이해하는 단계이며, Node.js가 가진 비동기 특성의 핵심과 단일 스레드 아키텍쳐, 논블로킹 I/O 같은 주요 개념들을 살펴보도록 하겠습니다. 먼저 I/O는 컴퓨터의 기본적인 동작들 중에 가장 느립니다. RAM에 접근하는 데에는 나노초인 반면, 디스크와 네트워크에 접근하는 데에는 밀리초가 걸립니다. CPU 측ㅁㅕㄴ에서는 I/O가 많은 비용을 요구하지 않지만 보내지는 요청과 작업이 완료되는 순간 사이의 지연이 발생할 뿐 아니라 실제 인간이라는 변수를 고려해야 하기 때문에 디스크와 네트워크보다 느릴 수 있습니다.

 

1. 블로킹 I/O

전통적인 블로킹 I/O 프로그래밍에서는 I/O를 요청하는 함수의 호출은 작업이 완료될 때까지 스레드의 실행을 차단합니다. 차단 시간은 디스크 접근의 경우 몇 밀리초부터 사용자가 키를 누르는것과 같은 사용자 액션에 의해서 데이터가 생성되는 경우 몇 분까지 소요되기도 합니다. 다음은 일반적인 블로킹 스레드를 보여줍니다.

// data가 사용가능해질 때 까지 블로킹
data = socket.read()
// data 사용 가능
print(data)

블로킹 I/O를 사용하여 구현된 웹 서버가 같은 스레드 내에서 여러 연결을 처리하지 못하는 것은 오류가 아닙니다. 소켓의 각각의 I/O 작업이 다른 연결의 처리를 차단하기 때문입니다. 이 문제를 해결하기 위한 전통적인 방법은 개별 스레드 또는 프로세스를 이용하여 처리 하는 방법이 있습니다.

하지만 스레드로 처리를 하게 되면 시스템 리소스 측면에서 비용이 저렴하지 않기 때문에 메모리와 CPU 사이클을 낭비하게 됩니다.

 

2. 논 블로킹 I/O

대부분의 최신 운영체제는 리소스에 접근하기 위해서 블로킹 I/O 외에도 논 블로킹 I/O라고 불리는 다른 메커니즘을 지원합니다. 이 운영모드에서 시스템 호출은 데이터가 읽혀지거나 쓰여지기를 기다리지 않고 항상 즉시 반환됩니다. 호출 순간에 사용 가능한 결과가 없는 경우, 함수는 단순히 미리 정의된 상수를 반환하여 그 순간에 사용 가능한 데이터가 없다는 것을 알립니다. 다음은 논 블로킹 I/O와 폴링 루프를 사용하여 여러 리소스로부터 읽어 들이는 것이 어떻게 가능한지 보여줍니다.

resources = [socketA, socketB, fileA]
while (!resources.isEmpty()) {
	for (resource of resources) {
   		// 읽기를 시도
        data = resource.read()
        if (data === NO_DATA_AVAILABLE) {
        	continue
        }
        if (data === RESOURCE_CLOSED) {
        	resouces.remove(resource)
        } else {
        	// 데이터를 받고 처리
            consumeData(data)
       }
    }   
}

보다시피 간단한 기법으로 서로 다른 리소스를 같은 스레드 내에서 처리할 수 있지만 폴링 알고리즘은 엄청난 CPU 시간의 낭비를 초래합니다.

 

3. 이벤트 디멀티플렉싱

 바쁜 대기(Busy-waiting)는 논 블로킹 리소스 처리를 위한 이상적인 방법이 아닙니다. 대부분의 운영체제는 효율적인 방안으로 동기 이벤트 디멀티플렉서 혹은 이벤트 통지 인터페이스라는 메커니즘을 제공합니다. 멀티플렉싱은 전기통신 용어로서 여러 신호들을 하나로 합성하여 제한된 수용범위 내에서 매개체를 통하여 쉽게 전달하는 방법을 나타냅니다. 반대로 디멀티플렉싱은 신호가 원래 구성요소로 다시 분할되는 작업입니다. 우리가 말하고 있는 동기 이벤트 디멀티플렉서는 여러 리소스를 관찰하고 이 리소스들 중에 일기 쏘는 쓰기 연산의 실행이 완료되었을 때 새로운 이벤트를 반환합니다.

 

4. 리액터 패턴

 우리는 이전 섹션에서 제시된 알고리즘에 특화된 리액터 패턴을 알아보겠습니다. 리액터 패턴의 이면에 있는 주 아이디어는 각 I/O 작업에 연관된 핸들러를 갖는다는 것 입니다. Node.js에서의 핸들러는 콜백 함수에 해당합니다.

 

위 그림은 리액터 패턴을 사용하는 애플리케이션에서 어떤 일이 발생하는지를 보여줍니다.

 

1. 애플리케이션은 이벤트 디멀티플렉서에 요청을 전달함으로써 새로운 I/O 작업을 생성합니다. 또한, 애플리케이션은 작업이 완료되었을 때, 호출된 핸들러를 명시합니다. 이벤트 디멀티플렉서에 새 요청을 전달하는 것은 논블로킹 호출이며, 제어권은 애플리케이션으로 즉시 반환됩니다.

2. 일련의 I/O 작업들이 완료되면 이벤트 디멀티플렉서는 대응하는 이벤트 작업들을 이벤트 큐에 집어 넣습니다.

3. 이 시점에서 이벤트 루프가 이벤트 큐의 항목들을 순환합니다. 

4. 각 이벤트와 관련된 핸들러가 호출됩니다.

5. 애플리케이션 코드의 일부인 핸들러의 실행이 완료되면 제어권을 이벤트 루프에 되돌려줍니다. 핸들러 실행 중에 다른 비동기 작업을 요청할 수 있으며, 이는 이벤트 디멀티플렉서에 새로운 항목을 추가하는 것입니다.

6. 이벤트 큐의 모든 항목이 처리되고 나면 이벤트 루프는 이벤트 디멀티플렉서에서 블로킹되며 처리 가능한 새 이벤트가 있을 경우 이 과정이 다시 트리거 됩니다.

 

 

# Node.js에서의 JavaScript


 앞서 분석한 아키텍처에서의 한 가지 중요한 점은 Node.js에서 사용하는 JavaScript는 브라우저에서 사용하는 JavaScript와는 다소 다르다는 것입니다.

 가장 눈에 띄는 차이점은 Node.js는 DOM을 가지고 있지 않으며, window와 document 또한 없다는 것입니다. 반면에 브라우저에서는 불가능하지만 Node.js는 운영체제에서 기본적으로 제공하는 서비스들에 접근이 가능합니다. 사실 브라우저는 악성 웹 애플리케이션에 의해서 기본 시스템이 손상되지 않도록 안전 조치가 적용되어 있습니다. 브루아저는 운영체제 리소스에 대해 높은 수준의 추상화를 제공하여 브라우저 안에서 실행되는 코드를 보유하고 조작하기 쉽게 하기 때문에 불가피하게도 사용에 제한이 있습니다. Node.js에서는 사실상 운영체제가 표출하는 거의 모든 서비스를 접근할 수 있습니다.

 

1. 최신 JavaScript를 실행시켜라

 브라우저에서 JavaScript를 사용할 때 주된 고충 중에 하나는 우리의 코드가 다양한 장치와 브라우저에서 실행되는 경향이 있다는 것입니다. 다른 브라우저를 사용한다는 것은 JavaScript 런타임이 프로그램 언어와 웹 플랫폼의 최신 특성들 중 몇가지를 간과할 수 있다는 것을 의미합니다. 다행히도 오늘날에는 트랜스파일러와 폴리필의 사용으로 이러한 문제들이 어느 정도 줄어들었습니다. 그럼에도 불구하고 이러한 것들은 여러 단점들을 가지고 있으며 모든 것들이 플로그인으로 대체 가능하지 않습니다.

 Node.js에서 애플리케이션을 개발할 때에는 이러한 모든 애로사항들이 적용되지 않습니다. 실제로 Node.js 애플리케이션이 이미 잘 알려진 시스템이나 Node.js 런타임 위에서 동작합니다. 이것이 만들어내는 엄청난 차이점은 우리가 JavaScript나 Node.js의 특정 버전에서 동작하는 코드를 사용할 수 있다는 것입니다. 

 이러한 요인과 함께 V8을 가지고 있다는 사실은 우리가 추가적인 소스 변환단계 없이 확신을 가지고 최신 ECMAScript 사양의 특성들 대부분을 사용 가능하다는 점입니다.

 

2. 운영체제 기능에 대한 모든 접근

 이미 언급했듯이 Node.js는 JavaScript를 사용하지만 운영체제에서의 기본적인 서비스를 제공합니다. 

 

 

# 다음 챕터에서는


 다음 챕터는 모듈 시스템에 대해 간략하게 배워보도록 하겠습니다. 모듈의 필요성과 패턴들, CommonJS 모듈과 ESM 모듈의 차이점, 상호 운용할 수 있는 기술까지 모두 배워보도록 하겠습니다.