NodeJs.개념 이해하기

2022. 12. 6. 16:02NodeJs

목차
1. NodeJs의 개념

  1.1. 서버
  1.2. 자바스크립트(JavaScript) 런타임
  1.3. 이벤트 기반(Event-Driven)
    1.3.1. 이벤트 루프

  1.4. 논 블로킹 I/O
  1.5. 싱글 스레드

 


1. NodeJs의 개념

 

<그림 1> 출처: NodeJs 공식 홈페이지

그림 1에서는 NodeJs를 공식홈페이지에서 소개하고 있는 글이다.

"Node.js(이하 노드)는 Chrome 8 JavaScript 엔진으로 빌드된 JavaScript 런타임입니다."

아직은 위의 문장을 보아도 잘 이해가 가지않는다. 이 문장을 이해하기 위해선 여러가지의 개념들이 필요하다.

 

이를 한번 알아보도록 하자.

 

 

1.1 서버

 

컴퓨터를 좀 사용해 봤다면 서버와 클라이언트에 대해 많이 들어봤을 것이다. 난 서버와 클라이언트에 대해 어렴풋이 무엇을 의미하는지 알지만, 정확히는 알지 못한다. 이에 대해 가볍게 한 번 알아보도록 하자.

 

`서버`란, 네트워크를 통해서 클라이언트에 정보나 서비스를 제공하는 컴퓨터 혹은 프로그램을 말한다.

 

<그림 2> 클라이언트와 서버

서버와 클라이언트를 결정하는 것은 요청과 응답을 누가 해주느냐에 따라 달라진다.

 

즉, 서버가 다른 서버에게 요청을 보낸다면 서버이면서 클라이언트일 수도 있다.

 

 

노드는 JavaScript 프로그램이 서버로서 동작할 수 있는 도구를 제공하므로 "서버" 역할을 수행할 수 있다.

 

 


1.2 자바스크립트(JavaScript) 런타임

 

NodeJs 공식 홈페이지의 문구를 다시 한번 읽어보자.

"Node.js(이하 노드)는 Chrome 8 JavaScript 엔진으로 빌드된 JavaScript 런타임입니다."

노드는 자바스크립트 런타임입니다. 런타임특정 언어로 만든 프로그램들을 실행할 수 있는 환경을 뜻한다.  이에 따라 노드는 자바스크립트 프로그램을 컴퓨터에서 실행할 수 있다. 쉽게 말해, 노드는 자바스크립트 실행기이다.

 

기존에는 자바스크립트를 브라우저 외의 환경에서 실행하기 위한 여러 시도가 있었으나, 실행 속도의 문제가 있었다. 하지만 구글의 V8 엔진을 사용하면서부터 이 문제가 해결되어 노드 프로젝트가 시작되었다.

 

<그림 3> 노드의 내부 구조

노드는 V8과 더불어 libuv라는 라이브러리를 사용하고, 이 두가지는 C와 C++로 구현되어있다.

우리가 작성한 코드는 노드가 알아서 V8과 libuv에 연결해주므로, C와 C++는 몰라도 상관없다.

 

이 중에서 libuv 라이브러리는 노드의 특성인 이벤트 기반, 논 블로킹 I/O 모델을 구현하고 있다.

 

 


1.3 이벤트 기반(event-Driven)

 

이벤트 기반(event-Driven)이란 이벤트가 발생할 때 미리 지정해둔 작업을 수행하는 방식을 의미한다.

이벤트로는 클릭이나 네트워크 요청 등이 있을 수 있다.

 

이벤트 기반 시스템에서는 특정 이벤트가 발생할 때 무엇을 할지 미리 등록해두어야한다. 이를 이벤트 리스너(eventListener)에 콜백(callback)함수를 등록한다고 표현한다.

 

노드는 이벤트 기반 방식으로 동작하므로, 이벤트가 발생하면 이벤트 리스너에 등록해둔 콜백함수를 호출한다.

발생한 이벤트가 없거나 발생했던 이벤트를 다 처리하면, 노드는 다음 이벤트가 발생할 때까지 대기한다.

 

 

<그림 4> 이벤트 기반에 대한 그림

 


 1.3.1 *이벤트 루프* (상당히 중요한 개념)

이벤트 기반 모델에서는 이벤트 루프(event-loop)라는 개념이 등장한다.

여러 이벤트가 동시에 발생했을 때 어떤 순서로 콜백 함수를 호출할지를 이벤트 루프가 판단한다.

 

노드는 자바스크립트 코드의 맨 위부터 한 줄씩 실행한다. 함수 호출 부분을 발견했다면,

호출한 함수를 호출 스택(call stack)에 넣는다.

 

이를 이해하기 위해서 다음의 코드가 console에 찍히는 순서를 예상해보도록 하자.

function first() {
    second()
    console.log("1")
}

function second() {
    third()
    console.log("2")
}

function third() {
    console.log("3")
}

first()

// "3"
// "2"
// "1"

 

<그림 5> 호출 스택

 

 

호출은 first, second, third 순으로 되지만, 실행이 되는 순서는 third, second, first 순으로 되어 "3". "2". "1" 순으로 콘솔된다.

가장 아래의 익명함수는 함수가 호출되었을 때 생성되는 환경을 의미하며, 실행이 되는 동안 스택에 있다가 실행이 끝나면 호출 스택에서 지워진다.

 

이번에는 특정 밀리초 이후에 코드를 실행하는 setTimeout을 사용하여,  console을 예측해보자.

function run () {
    console.log('3초 후 실행')
}

console.log('시작')

setTimeout(run, 3000)

console.log('끝')

// '시작'
// '끝'
// '3초 후 실행'

 

3초 뒤에 실행이 되므로 결과는 예측이 가능했지만, 호출 스택하나로는 이해가 불가능한 측면이 있다.

이를 이해하기 위해서는 이벤트 루프(event-loop), 백그라운드(background), 태스크 큐(task queue)를 알아야한다.

 

 

  • 이벤트 루프(event-loop)
    • 이벤트 발생 시 호출할 콜백 함수들을 관리하고, 호출된 콜백 함수의 실행 순서를 결정하는 역할을 담당한다.
    • 노드가 종료될 때까지 이벤트 처리를 위한 작업을 반복하므로 루프(loop)라고 부른다.
  • 백그라운드(background)
    • setTimeout 같은 타이머나 이벤트 리스너들이 대기하는 곳이다.
    • 자바스크립트가 아닌 다른 언어로 작성된 프로그램이라고 봐도된다.
    • 여러 작업이 동시에 실행될 수 있다.
  • 태스크 큐(task queue)
    • 이벤트 발생 후, 백그라운드에서는 태스크 큐로 타이머나 이벤트 리스너의 콜백 함수를 보낸다.
    • 정해진 순서대로 콜백들이 줄을 서 있으므로 콜백 큐라고도 부른다.
    • 콜백들은 보통 완료된 순서대로 줄을 서 있지만 특정한 경우에는 순서가 바뀌기도 한다.

 

다음의 그림은 코드가 실행되는 내부 과정을 묘사 했다.

<그림 6> 이벤트 루프 1번

 

  1. 먼저, 익명함수가 호출 스택에 들어가고, 그 뒤에 setTimeout이 호출 스택에 들어간다.
  2. 호출 스택에 들어간 순서와 반대로 실행되므로, setTimeout이 먼저 실행된다.
  3. setTimeout이 실행되면 타이머와 함꼐 run  콜백을 백그라운드로 보내고,
  4. setTImeout은 호출 스택에서 빠진다. 그 다음으로 익명함수가 호출 스택에서 빠진다.
  5. 백그라운드에서는 3초를 센 후 run함수를 태스크 큐로 보낸다.

 

3초를 세었다는 것은 백그라운드에 맡겨진 작업이 완료된 것으로 이해해도된다.

 

그림에서는 태스크 큐가 하나의 큐처럼 보이지만 실제로는 여러 개의 큐로 이루어져 있다.

 

이벤트 루프는 정해진 규칙에 따라 콜백함수들을 호출 스택으로 부른다.

 

다음 그림은 호출 스택에서 익명함수까지 실행이 완료되어 호출 스택이 비어 있는 상황을 보여 준다.

 

이벤트 루프는 호출 스택이 비어 있으면 태스크 큐에서 함수를 하나씩 가져와 호출 스택에 넣고 실행한다.

 

<그림 7> 이벤트 루프 2번

 

마지막 그림은 이벤트 루프가 run 콜백을 태스크 큐에서 꺼내 호출 스택으로 올린 상황이다.

 

호출 스택으로 올려진 run은 실행되고, 실행 완료 후 호출 스택에서 비워진다.

 

이벤트 루프는 태스크 큐에 콜백 함수가 들어올 때까지 계속 대기한다.

 

<그림 8> 이벤트 루프 3

 

만약 호출 스택에 함수들이 너무 많이 들어 있으면 3초가 지난 후에도 run 함수가 실행되지 않을 수 있다.

 

이벤트 루프는 호출 스택이 비어 있을 때만 태스크 큐에 있는 run 함수를 호출 스택으로 가져오기 때문이다.

 

이런 이유로 setTimeout의 시간이 정확하지 않을 수 있다.

 


1.4 논 블로킹 I/O

 

이벤트 루프를 잘 활용하면 오래 걸리는 작업을 효율적으로 처리할 수 있다.

 

작업은 두 가지 종류로 구분할 수 있는데,

  • 동시에 실행될 수 있는 작업
  • 동시에 실행될 수 없는 작업

기본적으로 자바스크립트 코드는 동시에 실행될 수 없지만, 자바스크립트상에서 돌아가는 것이 아닌, I/O 작업 같은 것은 동시에 처리될 수 있다.

 

I/O는 Input(입력) / Output(출력) 을 의미한다. 파일 시스템 접근이나 네트워크 요청 같은 작업이 I/O의 일종이다. 이러한 작업을 할 때 노드는 논 블로킹 방식으로 처리하는 방법을 제공한다.

 

논 블로킹이란 이전 작업이 완료될 때 까지 대기하지 않고 다음 작업을 수행함을 뜻한다.

반대로 블로킹은 이전 작업이 완료되어야 다음 작업을 수행할 수 있다.

 

다음의 그림을 보며 이해해보자.

<그림 9> 블로킹과 논 블로킹

 

논 블로킹 방식이 같은 작업을 처리할 때 더욱 빠르다는 것을 알 수 있다. 다만, 작업들이 모두 동시에 처리될 수 있는 작업이어야한다.

 

노드는 I/O 작업을 백그라운드로 넘겨 동시에 처리하곤 한다.

동시에 처리될 수 있는 작업들은 최대한 묶어서 백그라운드로 넘겨야 시간을 절약할 수 있다.


다음의 그림은 순서에 따른 작업 시간을 나타냈다.

<그림 10> 작업 순서의 중요성

 

1, 3, 5는 동시에 작업이 이루어질 수 있고, 2, 4는 동시에 수행이 불가능하다.

 

이 때 순서를 CASE 1과 같이 배치하면, 동시에 처리가 가능한 이점을 볼 수 없다.

 

CASE 2와 같이 동시에 처리가 가능한 작업들 끼리 순서를 붙여야 시간적 이득을 볼 수 있다.

 

즉, 작업 순서에 따른 성능의 차이는 크게 달라진다.

 

동시에 처리가 가능한 I/O 작업이라도 논 블로킹 방식으로 코딩하지 않으면 의미가 퇴색되므로

 

논 블로킹 방식으로 코딩하는 습관을 가져야한다.


논 블로킹 방식의 코드를 예제로 알아보자.

 

우선 블로킹 방식의 코드이다. 순서를 예측해보자.

function longRunningTask(){
	// 오래 걸리는 작업
    console.log('작업 끝')
}

console.log('시작')
longRunningTask()
console.log('다음 작업')

// '시작'
// '작업 끝'
// '다음 작업'

첫번쨰 작업인 '시작'을 출력하고

 

시간이 오래걸리는 두 번쨰 작업 longRunningTask()를 호출,

 

그 후에 마지막 작업인  '작업 끝'을 출력한 후에 '다음 작업'을 출력한다.

 

 

즉, 시간이 오래 걸리더라도 오래 걸리는 두 번째 작업이 끝나야 그 다음 작업을 수행한다.

 

 

이번에는 논 블로킹 방식의 I/O 작업을 수행한다고 생각해보자.

function longRunningTask(){
	// 오래 걸리는 작업
    console.log('작업 끝')
}

console.log('시작')
setTimeout(longRunningTask, 0)
console.log('다음 작업')

// '시작'
// '다음 작업'
// '작업 끝'

첫번쨰 작업인 '시작'을 출력하고, 두 번쨰 작업을 백그라운드로 보낸다.

 

그 후 세 번째 작업인 '다음 작업'을 출력하고,

 

백그라운드에서 태스크 큐를 통해 호출 스택으로 넘어가 '작업 끝'이 출력된다.

 

 

setTimeout(callback, 0)은 코드를 논 블로킹으로 만들기 위해 사용하는 기법 중 하나다.

 

노드에서는 setTimeout보다는 setImmediate 같은 방식을 사용한다.

 

위의 기법으로 인해 다음 작업을 먼저 실행, 완료 후에 오래 걸리는 작업을 완료한다.

 

단, 아무리 논 블로킹 방식이라고 하더라도, 동시에 실행되지 않으면 소요시간이 짧아지지 않는다.

 

실행 순서만 바뀔뿐이다.

 

하지만 I/O 작업이 없다고 해서 논 블로킹이 의미가 없는 것은 아니다. 실행 순서를 바꿔줌으로써 간단한 작업들을 대기 시키지 않고 먼저 처리할 수 있다는 의의가 있다.

 

또한 논블로킹과 동시는 같은 의미가 아니며, '동시성'은 동시 처리가 가능한 작업을 논 블로킹 처리해야 얻을 수 있다.

 

  setTImeout(callback, 0)
밀리초를 0으로 설정해 바로 실행된다고 착각이 가능하지만, 브라우저와 노드에서 각각 기본적인 지연시간이 있어 바로 실행되지 않는다. 브라우저에서는 4ms, 노드에서는 1ms의 지연 시간이 존재한다.

1.5 싱글 스레드(Single_Thread)

 

싱글 스레드란 스레드가 하나뿐이라는 것을 의미한다.

 

스레드를 이해하기 위해서는 프로세스를 먼저 알아야한다.

프로세스는 운영체제에서 할당하는 작업의 단위이다.
노드나 브라우저 같은 프로그램은 개별적인 프로세스이며, 프로세스 간에는 메모리 등의 자원을 공유하지 않는다.

 

스레드는 프로세스 내에서 실행되는 흐름의 단위이다.
프로세스는 스레드를 여러개 생성해 여러 작업을 동시에 처리할 수 있다. 스레드들은 부모 프로세스의 자원을 공유하고, 같은 주소의 메모리에 접근이 가능해 데이터를 공유할 수 있다.

 

<그림 11> 스레드와 프로세스

 

노드는 싱글스레드라고 알려져 있다. 그러나 정확하게는 싱글스레드로 동작하지 않는다.

노드를 실행하면 먼저 프로세스가 하나 생성된다. 그리고 그 프로세스에서 스레드들을 생성한다.

이 중에서 제어가 가능한 스레드는 한 개이다.

 

즉, 싱글 스레드라고 하는 것은 제어할 수 있는 스레드가 하나이기 때문이다.


스레드는 작업을 처리하는 일손으로도 표현한다. 하나의 스레드만 직접 조작할 수 있으므로 일손이 하나인 셈이다.

요청이 많이 들어오면 한 번에 하나씩 요청을 처리한다.

 

블로킹이 심하게 일어나는 작업을 처리하지만 않는다면 스레드 하나로도 충분하다.

블로킹이 발생할 것 같은 경우에는 논 블로킹 방법으로 대기 시간을 최대한 줄여야한다.

 

언뜻보면 싱글스레드보다 멀티스레드가 좋아 보이지만, 반드시 그런 것은 아니다.

 

장단점이 있으므로, 상황에 알맞게 사용해야한다.

 


1.5.1 싱글 스레드 블로킹

 

한 음식점에 점원이 한명, 손님이 여러명이라 가정해보자.

점원 한명은 주문을 받아  주방에 넘기고, 주방에서 음식이 나오면 손님에게 서빙을 한다.

완료 후 다음 손님의 주문을 받아, 주방에 넘기고 음식이 나오면 서빙을 한다.

 

이런 방식으로 작업을 수행하게되면, 손님들은 이전 손님의 음식이 나오기 전까지는 아무것도 하지 못하고 기다려야한다.

 

<그림 12> 싱글 스레드 블로킹

 

이것이 싱글 스레드 블로킹 모델로서, 매우 비효율적이다.

 


1.5.2 싱글 스레드 논 블로킹

 

이번에는 같은 상황에서 작업 방식을 바꿔보자.

 

주문을 받아 주방에 넘기고, 바로 다음 손님의 주문을 받아 주방에 넘긴다.

음식이 나오기를 기다리지 않고 주문 내역을 주방에 전달하는 것이다.

그리고 주방에서 요리가 나오면, 나오는 순서대로 음식을 서빙한다.

 

<그림 13> 싱글 스레드, 논 블로킹

 

이것이 싱글 스레드 논 블로킹 모델이며, 노드가 채택하고 있는 방식이다.

 

하지만, 만약에 점원 하나가 아프거나 쓰러진다면 큰 문제가 생긴다.

또한, 요리를 하는 데 시간이 오래 걸린다면(CPU를 많이 쓰는 작업) 주문이 많이 들어왔을 때 버거울 수 있다.

 


1.5.3 멀티 스레드 블로킹

 

멀티 스레드 방식은 각각의 손님마다 주문을 받을 점원을 배치하는 것이다.

 

언뜻보면 좋은 방식 같아 보이지만, 손님이 없다면 노는 점원도 생기고,

점원을 고용하는데에 비용이 더 들기떄문에 좋지 못한 방식이다.

 

<그림 14> 멀티 스레드, 블로킹

 


1.5.4  멀티 스레드 논 블로킹  → 멀티 프로세싱

 

그렇다면 멀티 스레드가 논 블로킹 방식으로 주문을 받으면 좋지 않을까 싶기도 하다.

실제로도 그러면 좋겠지만, 멀티 스레드 방식으로 프로그래밍을 하는 것은 상당히 어렵다.

 

그래서 멀티 프로세싱 방식을 대신 사용한다. I/O 요청에서는 멀티 프로세싱이 더 효율적이기도 하다.

 

아래의 표는 멀티 스레딩과 멀티 프로세싱을 비교한 것이다.

 

멀티 스레딩 멀티 프로세싱
하나의 프로세스 안에서 여러 개의 스레드 사용 여러 개의 프로세스 사용
CPU 작업이 많을 때 사용 I/O 요청이 많을 때 사용
프로그래밍이 어려움 프로그래밍이 비교적 쉬움

 

I/O 작업을 할 때는 멀티 스레딩보다 멀티 프로세싱이 효율적이므로 노드는 멀티 프로세싱을 많이 한다.

 

 

 

출처: Node.js 교과서

'NodeJs' 카테고리의 다른 글

NodeJs.TCP Server 만들기(2)  (0) 2022.12.17
NodeJs.TCP Server 만들기(1)  (0) 2022.12.15
NodeJs.TCP 흐름  (0) 2022.12.15
NodeJs.모듈  (0) 2022.12.06
NodeJs.실행 및 사용방법  (0) 2022.12.06