Nodejs

Chapter01 - Node.js 플랫폼

woo__c.s 2021. 7. 20. 23:49

경량 코어

 Node.js 코어는 몇 가지 원칙들을 기반으로 자신의 기초를 구성하였다.

 최소한의 기능 세트를 가지고 코어의 바깥부분에 유저랜드(userland) 혹은 

 유저스페이스(userspace)라 불리는 사용자 전용 모듈 생태계를 두는 것. 

 코어를 최소한의 기능 세트로 관리하는 것은 관리의 관점에서 편리할 뿐 아니라

 전체 생태계 진화에 있어 긍정적인 문화적 영향을 미칠 수 있다.

 

경량 모듈

 Node.js는 프로그램 코드를 구성하는 기본적인 수단으로서 모듈 개념을 사용한다.

 이것은 애플리케이션과 재사용 가능한 라이브러리를 만들기 위한 구성 요소이다.

 Node.js에서 가장 널리 통용되는 원칙 중 하나는 코드의 양 뿐 아니라 범위의 측면에서도

 작은 모듈을 디자인 하는 것이다.

 Node.js는 패키지 관리자(npm, yarn)로 패키지가 자신이 필요로 하는 버전의 종속성 패키지들을

 갖도록 함으로써 종속성 지옥에서 벗어나게 해준다.

 이러한 것이 재사용성 측면을 향상 시킨다.

 작은 모듈은 재사용성이라는 장점 외에도 다음의 장점이 있다.

  • 이해하기 쉽고 사용하기 쉽다.
  • 테스트 및 유지보수가 쉽다.
  • 사이즈가 작아 브라우저에서 사용하기에 완벽하다

 

 더 작고 집중화된 모듈을 갖는 것은 모두에게 공유와 재사용을 가능하게 해준다.

 이것은 오나전히 다른 수준에 적용된 Don't Repeat Yourself(DRY) 원칙이다.

 

작은 외부 인터페이스

 Node.js의 모듈들이 갖는 장점은 작은 사이즈와 작은 범위 그리고 최소한의 기능 노출이다.

 이러한 것들이 명확하게 사용될 수 있고 잘못된 사용에 덜 노출되도록 하는

 API 생산 효과를 갖는다. Node.js에서 모듈을 정의하는 가장 일반적인 패턴은

 명백한 단일 진입점을 제공하기 위해서 단 하나의 함수나 클래스를 노출시키는 것이다.

 Node.js의 많은 모듈들의 특징 중 또 다른 하나는 그들이 확장보다는

 사용되기 위해서 만들어 졌다는 것이다. 확장의 가능성을 금지하기 위해 모듈 내부 접근을

 제한한다는 것이 덜 유연하다고 생각되지만 사실은 유스케이스를 줄이고, 구현을 단순화하며,

 유지관리를 용이하게 하고, 가용성을 높인다는 장점들을 가지고 있다. 

 실제로 이는 내부를 외부에 노출시키지 않기 위해 클래스보다 함수를 

 노출시키는 것을 선호한다는 것을 의미한다.

 

간결함과 실용주의

  "디자인은 구현과 인터페이스 모두에서 단순해야 한다. 구현이 인터페이스보다 더 단순

    해야 한다는 것은 더욱 중요하다. 단순함이 디자인에서 가장 중요한 고려 사항이다."

   리차드 가브리엘                                            

 

 여러 이유로 인해서 완벽하고 모든 기능을 갖춘 소프트웨어와는 반대로 단순하게 설계하는 것

 이 좋은 실천이다. 구현을 위해서 적은 노력이 들고, 가벼워서 빨리 보급 가능하며, 유지보수가 

 쉽고 빠른 이애하 가능하다. 이러한 요인들이 커뮤니티의 기여를 보다 용이하게 하고 소프트웨어

 자체의 성장과 향상을 돕는다.

 Node.js에서 이 원칙이 채택되는 것에 가장 큰 영향을 준 것은 Javascript이다.

 

I/O는 느리다

 I/O는 컴퓨터의 기본적인 동작들 중에서 가장 느리다.

 RAM에 접근하는데 나노초 (10^-9초),

 디스크와 네트워크에 접근하는 데에는 밀리초(10^-3초)

 대역폭도 마찬가지이다. RAM의 전송률은 GB/s 단위로 일관되게 유지되는 반면,

 디스크나 네트워크의 전송률은 MB/s에서 GB/s까지 다양하다.

 CPU의 측면에서는 I/O가 많은 비용을 요구하지 않지만 보내지는 요청과 작업이 완료되는 순간

 사이의 지연이 발생하게 된다.

 

블로킹 I/O

 전통적인 블로킹 I/O 프로그래밍에서느느 I/O를 요청하는 함수의 호출은 작업이 완료될 때까지

 스레드의 실행을 차단한다. 사용자 액션에 의해서 데이터가

 생성되는 경우 몇 분 까지 소요되기도 한다. 블로킹 I/O를 사용하여 구현된

 웹 서버가 같은 스레드 내에서 여러 연결을 처리하지 못하는 것은 자명한 일이다.

 소켓의 각각의 I/O 작업이 다른 연결의 처리를 차단하기 때문이다.

 이 문제를 해결하기 위한 전통적인 접근 방법은 각각의 동시 연결을 처리하기 위해서

 개별의 스레드 또는 프로세스를 사용하는 것이다.

 이 방법은 I/O 작업이 각각의 스레드에서 처리되기 때문에 I/O 작업으로 인해 블로킹된

 스레드가 다른 연결들의 가용성에 영향을 미치지 않는다.

 

논 블로킹 I/O

 대부분의 최신 운영체제는 리소스에 접근하기 위해서 블로킹 I/O 외에도 논 블로킹 I/O라고 

 불리는 다른 메커니즘을 지원한다. 이 운영모드에서 시스템 호출은 데이터가

 읽혀지거나 쓰여지기를 기다리지 않고 항상 즉시 반환된다.

 호출 순간에 사용 가능한 결과가 없는 경우, 함수는 단순히 미리 정의된

 상수를 반환하여 그 순간에 사용 가능한 데이터가 없다는 것을 알린다.

 논 블로킹 I/O를 다루는 가장 기본적인 패턴은 실제 데이터가 반환될 때까지 루프 내에서 

 리소스를 적극적으로 폴링(poll)하는 것이다. 이것을 바쁜 대기(busy-waiting)이라고 한다.

 폴링 알고리즘은 엄청난 CPU 시간의 낭비를 초래한다.

 

이벤트 디멀티플렉싱

 바쁜 대기는 논 블로킹 리소스 처리를 위한 이상적인 기법이 아니다.

 대부분의 운영체제는 논 블로킹 리소스를 효율적인 방법으로 처리하기 위한 기본적인

 메커니즘을 제공한다. 이 메커니즘을

 동기 이벤트 디멀티플렉서 또는 이벤트 통지 인터페이스라고 한다.

 동기 이벤트 디멀티플렉서는 여러 리소스를 관찰하고 이 리소스들 중에 읽기 또는

 쓰기 연산의 실행이 완료되었을 때 새로운 이벤트를 반환한다.

 동기 이벤트 디멀티플렉서가 처리하기 위한 새로운 이벤트가 있을 때까지 블로킹 된다.

 이 패턴을 이용하면 바쁜 대기 기술을 이용하지 않고도 여러 I/O 작업을 단일 스레드

 내에서도 다룰 수 있다는 것이다. 

 실제로 하나의 스레드만 가지는 것은 일반적으로 프로그래머가 동시성에 접근하는 

 방식에 이로운 영향을 미치게 된다. 

 

리액터(Reactor) 패턴

 리액터 패턴의 이면에 있는 주된 아이디어는 각 I/O 작업에 연관된 핸들러를 갖는다는 것이다.

 Node.js 에서의 핸들러는 콜백 함수에 해당한다.

 애플리케이션은 특정 시점에 리소스로(블로킹 없이) 접근하고 싶다는 요청과 동시에

 작업이 완료되었을 때 호출될 핸들러를 제공한다.

 Node.js 애플리케이션은 이벤트 디멀티플렉서에 더 이상 보류중인 작업이 없고 이벤트 큐에

 더 이상 처리 중인 작업이 없을 경우 종료된다.

 Reactor 패턴 : 일련의 관찰 대상 리소스에서 새 이벤트를 사용할 수 있을 때까지 블로킹하여

                     I/O를 처리하고, 각 이벤트를 관련된 핸들러에 전달함으로써 반응한다.

 

Libuv, Node.js의 I/O 엔진

 각 운영체제는 Linux의 epoll, macOS의 kqueue, Window의 IOCP(I/O completion port)

 API와 같은 이벤트 디멀티플렉서를 위한 자체 인터페이르를 가지고 있다.

 게다가 각 I/O 작업은 동일한 OS 내에서도 리소스 유형에 따라 다르게 동작할 수 있다.

 서로 다른 운영체제 간의 불일치성은 이벤트 디멀티플렉서를 위해 보다 높은 레벨의 추상화를

 필요로하게 되었고, 이러한 이유로 Node.js 코어 팀이 Node.js 를 주요 운영체제에서 호환되게

 해주며 서로 다른 리소스 유형의 논 블로킹 동작을 표준화하기 위해 libuv라고 불리는 

 C라이브러리를 만들었다. Libuv는 Node.js의 하위 수준의 I/O 엔진을 대표하며 아마도

 Node.js의 구성요소 중에서 가장 중요하다고 말할 수 있다.

 Libuv는 기본 시스템을 호출을 추상화하는 것 외에도 리액터 패턴을 구현하고 있으므로 

 이벤트 루프의 생성, 이벤트 큐의 관리, 비동기 I/O 작업의 실행 및 다른 유형의 작업을

 큐에 담기 위한 API들을 제공한다.

 

Node.js를 위한 구성

 리액터 패턴과 libuv는 Node.js의 기본 구성 요소지만 전체 플랫폼의 구축을 위해서는

 3개의 구성이 더 필요하다.

  • libuv와 다른 저수준 기능들을 랩핑하고 표출히키기 위한 바인딩 세트
  • V8
  • 고수준 Node.js API를 구현하고 있는 코어 Javascript 라이브러리

 

Node.js 에서의 Javascript

 Node.js 에서 사용하는 Javascript는 브라우저에서 사용하는 Javascript와는 다소 다르다.

 Node.js 는 DOM을 가지고 있지 않으며, window와 document 또한 없다.

 브라우저에서는 불가능하지만 Node.js는 운영체제에서 기본적으로 제공하는 

 서비스들에 접근이 가능하다. 

 브라우저는 악성 웹 애플리케이션에 의해서 기본 시스템이 손상되지 않도록 안전 조치가

 적용되어있다. 브라우저는 운영체제 리소스에 대해 높은 수준의 추상화를 제공하여 

 브라우저 안에서 실행되는 코드를 보유하고 조작하기 쉽게 하기 때문에 불가피하게도

 사용에 제한이 있다. Node.js 에서는 사실상 운영체제가 표출하는 거의 모든 서비스에

 접근할 수 있다. 

 

최신 Javascript를 실행시켜라

 브라우저에서 Javascript를 사용할 때 주된 고충 중에 하나는 코드가 다양한 장치와 브라우저에서

 실행되는 경향이 있다는 것이다. 다른 부라우저를 사용한다는 것은 Javascript 런타임이

 프로그램 언어와 웹 플랫폼의 최신 특성들 중 몇가지를 간과할 수 있다는 것을 의미한다.

 오늘날에는 트랜스파일러와 폴리필의 사용으로 이러한 문제들이 어느 정도 줄어들었다.

 그럼에도 불구하고 이러한 것들은 여러 단점들을 가지고 있으며 

 모든 것들이 플러그인으로 대체 가능하지 않다.

 Node.js 에서 애플리케이션을 개발할 때에는 이러한 모든 애로사항들이 적용되지 않는다.

 실제로 Node.js 애플리케이션이 이미 잘 알려진 시스템이나 Node.js 런타임 위에서 동작한다.

 이것이 만들어내는 차이점은 Javascript나 Node.js의 특정 버전에서 동작하는 코드를

 사용할 수 있다는 것이다. 

 이러한 요인과 함께 Node.js가 가장 최신버전의 V8을 가지고 있다는 사실은 우리가 추가적인

 소스 변환단계 없이 확신을 가지고 최신 ECMAScript 사양의 특성들 대부분을 사용 가능하다.

 만약 서드파티에서 사용되기 위한 라이브러리를 개발한다면 우리는 여전히 우리의 코드가

 다양한 Node.js 버전에서 실행될 수 있음을 고려해야한다. 이 경우에 일반적인 패턴은

 LTS(long-term support) 지원 버전 중에서 가장 오래된 것을 기준으로 삼고 

 package.json에 engines 섹션을 명시하는 것이다. 패키지 매니저는 사용자가 해당 Node.js

 버전에 호환하지 않는 패키지를 설치하려고 할 때 경고할 것이다.

 

모듈 시스템

 Javascript가 여전히 어떠한 형식에 공식적인 지원이 없었을 때 Node.js는 모듈 시스템과

 함께 시작되었다. 본래의 Node.js 모듈 시스템은 CommonJS로 불렷으며 내장 모듈 또는

 장치의 파일시스템에 위치한 모듈로부터 외부에 표출된 함수와 변수 그리고 클래스를 임포트

 하기 위해서 require 키워드를 사용한다.

 CommonJS는 클라이언트 사이드 세계에서 코드 번들을 만들고 브라우저에서 쉽게 실행

 될 수 있게 해주는 Webpack이나 Rollup과 같은 모듈 번들러의 사용과 함께 시작되었다.

 CommonJS는 개발자들이 다른 서버 사이드 플랫폼처럼 크고 잘 구조화된

 애플리케이션을 만들기 위해서 필수적인 구성요소였다.

 오늘날에 Javascript는 (import 키워드라고 하면 더욱 친숙한) 소위 ES 모듈 문법이라고

 불리는 것을 가지고 있다. 이것은 브라우저에서와는 기본적으로 구현이 다른 것으로 

 Node.js에서는 문법만 상속받는다. 실제로 브라우저가 원격에 있는 모듈을 주로 다루는 반면,

 Node.js는 현재로는 오직 로컬 파일 시스템에 있는 모듈만 다룰 수 있다.

 

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

 Node.js는 Javascript를 사용하지만 브라우저 영역 안에서 실행되지 않는다.

 이것은 Node.js가 운영체제에서 기본적으로 제공하는 주된 서비스들에 바인딩할 수 있게해준다.

 예를 들어 fs모듈의 도움으로 파일시스템에 있는 파일에 접근 가능하며 het 과 dgram 모듈로

 애플리케이션이 저수준의 TCP 또는 UDP 소켓을 사용하게 할 수 있다.

 HTTP(S) 서버를 만들 수 있고 표준 암호화와 OpenSSL의 (crypto 모듈을 사용) 해시 알고리즘을 

 사용할 수 있다. v8 모듈을 사용해서 V8내부 여러곳에 접근이 가능하며

 vm 모듈을 사용해서 V8 의 다른 문맥 상에서 코드를 실행시킬 수 있다.

 

네이티브 코드 실행

 Node.js가 제공하는 가장 강력한 기능 중 하나는 네이티브 코드에 바인드할 수 있는

 사용자측 모듈을 만들어 내는 것이 가능하다는 것이다. 

 Node.js 는 N-API 인터페이스의 도움으로 네이티브 모듈을 구현하는데 이써서 강력한 지원을 한다.

 하드웨어 드라이브나 하드웨어 포트(USB나 시리얼과 같은)의 저수준 특성들에 

 접근이 여전히 필요하다는 것이다. 

 Node.js 는 네이티브 코드와 연결될 수 있는 덕분에 사물인터넷이나 홈메이드 로보틱스 세계에서

 인기를 얻고 있다. 

 V8은 Javascript 실행에 있어서 매우 빠르게 동작하지만 네이티브 코드와 비교 했을 때 여전히 

 수행능력에 손실이 따른다. 엄청난 양의 데이터를 처리하고 조작하여 네이티브 코드에

 위임하는 CPU집약적 애플리케이션의 경우에는 충분히 마주칠 수 있는 문제이다.

 현재 Node.js를 포함한 대부분의 Javascript 가상머신은 Javascript 이외의 언어(C++ , Rust)를

 Javascript VM들이 "이해가능"한 형식으로 컴파일 할 수 있게 해주는

 저수준 명령 형식인 웹어셈블리(WASM)를 지원한다.

 

 

위 내용은 Node.js 디자인 패턴 바이블 책을 보고 내용을 정리한 것 입니다.

더 자세한 내용은 책을 구매하셔서 보는것이 좋을 것 같으며

문제 발생시 삭제하겠습니다.

'Nodejs' 카테고리의 다른 글

Chapter02- 모듈 시스템  (0) 2021.07.22