node.js의 비동기적 작동은 어디로부터 왔는가(1)

김성윤·2023년 4월 24일
0

이 글은 node.js 디자인 패턴 바이블 도서를 읽고 정리한 내용입니다

Node.js 철학

node.js를 특별하게 만드는 중요한 측면들 중 하나는 그것의 철학이다.

경량 코어

nodejs 코어는 몇가지 원칙들을 기반으로 기초를 구성하였다.
그 중 한 가지는 최소한의 기능 세트를 가지고 코어의 바깥부분에 유저랜드(userland) 혹은 유저스페이스(userspace)라 불리는 사용자 전용 모듈 생태계를 두는 것이었다.

최소한의 기능은 기본으로 제공하는 모듈들(http,os 등등)로 보임.원칙들이 더 궁금한데, 찾아볼 수가 없다


이것들이 위에서 말하는 "최소한의 기능 세트" 아닐까?
출처 : https://tutorialpost.apptilus.com/code/posts/nodejs/nodejs-modules/

경량 모듈

node.js는 프로그램 코드를 구성하는 기본적인 수단으로서 모듈 개념을 사용한다.
이것은 애플리케이션과 재사용 가능한 라이브러리를 만들기 위한 구성 요소이다.
가장 널리 통용되는 원칙 중 하나는 코드의 양 뿐 아니라 범위의 측면에서도 작은 모듈을 디자인하는 것이다

이 원칙은 Unix 철학에 근거하는데, 특히 두 가지 수칙이 있다.

  • 작은 것이 아름답다.
  • 각 프로그램은 한가지 역할만 잘 하도록 만들어라.

node.js는 패키지 관리자(npm, yarn)의 도움을 받아 충돌의 위험 없이 잘 집중화 된 많은 수의 작은 종속성을 가질 수 있도록 해준다.

다른 플랫폼에서는 비실용적이고 적용 불가능한 반면, node.js는 이러한 관행이 표준이다.

작은 모듈은 재사용성이라는 장점 외에도

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

라는 장점들이 있다
이것은 완전히 다른 수준에서 적용된 Don't Repeat Yourself(DRY) 원칙이다.

작은 외부 인터페이스

node.js의 모듈들이 갖는 장점은 작은 사이즈와 작은 범위 그리고 최소한의 기능 노출이다.
node.js에서 모듈을 정의하는 가장 일반적인 패턴은 명백한 단일 진입점을 제공하기 위해서 단 하나의 함수나 클래스를 노출시키는 것이다
모듈들의 특징 중 또 다른 하나는, 확장보다는 사용을 위해 만들어졌다는 것이다. 확장의 가능성을 금지하기 위해 모듈 내부 접근을 제한한다는 것이 덜 유연해보이지만,유스케이스를 줄이고, 구현을 단순화하며, 유지관리를 용이하게하고 가용성을 높인다는 장점들을 가지고 있다.
이는 클래스보다 함수를 노출시키는 것을 선호한다는 것을 의미함

간결함과 실용주의

디자인은 구현과 인터페이스 모두에서 단순해야한다. 구현이 인터페이스보다 더 단순해야 한다는 것은 더 중요하다. 단순함이 디자인에서 가장 중요한 고려사항이다
많은 노력과 유지보수가 필요한 엄청난 양의 코드로 완벽에 가까운 소프트웨어를 만들어 내려고 애쓰는 것보다, 합리적인 복잡성을 가지고 빠르게 일할 때 더 많은 성공을 이뤄낼 것이다.

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

I/O는 느리다

RAM에 접근하는 것은 나노초인 반면, 디스크와 네트워크에 접근하는 데에는 밀리초가 걸린다. 대역폭도 마찬가지.

블로킹 I/O

전통적인 블로킹 I/O는 작업이 완료될 때까지 스레드의 실행을 차단한다.

스레드의 개념
프로세스(process)란?
프로세스(process)란 단순히 실행 중인 프로그램(program)이라고 할 수 있습니다.
즉, 사용자가 작성한 프로그램이 운영체제에 의해 메모리 공간을 할당받아 실행 중인 것을 말합니다.
이러한 프로세스는 프로그램에 사용되는 데이터와 메모리 등의 자원 그리고 스레드로 구성됩니다.
스레드(thread)란?
스레드(thread)란 프로세스(process) 내에서 실제로 작업을 수행하는 주체를 의미합니다.
모든 프로세스에는 한 개 이상의 스레드가 존재하여 작업을 수행합니다.
또한, 두 개 이상의 스레드를 가지는 프로세스를 멀티스레드 프로세스(multi-threaded process)라고 합니다. 출처http://www.tcpschool.com/java/java_thread_concept

그렇기 때문에 동시 연결을 처리하기 위해 개별의 스레드 또는 프로세스를 사용한다.

메모리를 소모하고 컨텍스트 전환을 유발하여 대부분의 시간 동안 사용하지 않는 장시간 실행 스레드를 가지게 됨으로써 귀중한 메모리와 CPU사이클을 낭비하게 된다.

논 블로킹 I/O

논 블로킹 I/O를 다루는 가장 기본적인 패턴은 실제 데이터가 반환될 때까지 루프 내에서 리소스를 적극적으로 폴링하는 것이다. 이것을 busy-waiting이라고 한다.

폴링(polling)이란 하나의 장치(또는 프로그램)가 충돌 회피 또는 동기화 처리 등을 목적으로 다른 장치(또는 프로그램)의 상태를 주기적으로 검사하여 일정한 조건을 만족할 때 송수신 등의 자료처리를 하는 방식을 말한다.
출처:https://ko.wikipedia.org/wiki/%ED%8F%B4%EB%A7%81_(%EC%BB%B4%ED%93%A8%ED%84%B0_%EA%B3%BC%ED%95%99)

이것은 효율적이지 않다. 사용할 수 없는 리소스를 반복하는 데에 소중한 CPU를 사용한다.

이벤트 디멀티플렉싱

대부분의 운영체제는 논 블로킹 리소스를 효율적으로 처리하기 위한 기본적인 메커니즘을 제공한다. 이 메커니즘을 동기 이벤트 디멀티플렉서 또는 이벤트 통지 인터페이스라고 한다
동기 이벤트 디멀티플렉서는 여러 리소스를 관찰하고 이 리소스들 중에 읽기 또는 쓰기 연산의 실행이 완료되었을 때 새로운 이벤트를 반환한다.

watchedList.add(socketA, FOR_READ) 								// (1)
watchedList.add(fileB, FOR_READ)
while (events = demultiplexer.watch(watchedList)) { 			// (2)
// 이벤트 루프
	for (event of events) { 									// (3)        
    	// 블로킹하지 않으며 항상 데이터를 반환        
        data = event.resource.read()        
        if (data === RESOURCE_CLOSED) {    // 리소스가 닫히고 관찰되는 리스트에서 삭제

        demultiplexer.unwatch(event.resource)
        
		} else {            // 실제 데이터를 받으면 처리
		consumeData(data)
        		}   
        }
 }

-알라딘 eBook <Node.js 디자인 패턴 바이블> (Mario Casciaro.Luciano Mammino 지음, 김성원 외 옮김) 중에서

  1. 각 리소스가 데이터 구조(List)에 추가됩니다. 각 리소스를 특정 연산과 연결합니다.
  2. 디멀티플렉서가 관찰될 리소스 그룹과 함께 설정됩니다. demultiplexer.watch()는 동기식으로 관찰되는 리소스들 중에서 읽을 준비가 된 리소스가 있을 때까지 블로킹됩니다. 준비된 리소스가 생기면, 이벤트 디멀티플렉서는 처리를 위한 새로운 이벤트 세트를 반환합니다.
  3. 이벤트 디멀티플렉서에서 반환된 각 이벤트가 처리됩니다. 이 시점에서 각 이벤트와 관련된 리소스는 읽을 준비 및 차단되지 않는 것이 보장됩니다. 모든 이벤트가 처리되고 나면, 이 흐름은 다시 이벤트 디멀티플렉서가 처리 가능한 이벤트를 반환하기 전까지 블로킹됩니다. 이를 이벤트 루프(event loop)라고 합니다

이것이 전체적인 유휴시간을 최소화시키는 데에 확실한 이점이 있다는 것이 명확히 나타난다.
이 책을 통해 경쟁 상태 발생 문제와 다중 스레드의 동기화 문제가 없다는 것이 어떻게 우리에게 더 간단한 동시성 전략을 사용하게 해 줄 수 있는지 보게 될 것이다

profile
Nest.js 백엔드 개발자

0개의 댓글