Node.js 란?

유석현(SeokHyun Yu)·2022년 11월 30일
0

Node.js

목록 보기
1/29
post-thumbnail

1. 자바스크립트 런타임

노드는 자바스크립트 런타임이다.

런타임은 특정 언어로 만든 프로그램들을 실행할 수 있는 환경을 뜻한다.

따라서 노드는 자바스크립트 프로그램을 컴퓨터에서 실행할 수 있게 해준다.

기존에는 자바스크립트 프로그램을 인터넷 브라우저(브라우저도 자바스크립트 런타임이다) 위에서만 실행할 수 있었다.

브라우저 외의 환경에서 자바스크립트를 실행하기 위한 여러가지 시도가 있었지만, 자바스크립트의 실행 속도 문제 때문에 모두 큰 호응을 얻지는 못했다.

하지만 2008년 구글이 V8 엔진을 사용하여 크롬을 출시하자 이야기가 달라졌다.

당시 V8 엔진은 다른 자바스크립트 엔진과 달리 매우 빨랐고, 오픈 소스로 코드도 공개되었다.

속도 문제가 해결되자 라이언 달(Ryan Dahl)은 2009년 V8 엔진 기반의 노드 프로젝트를 시작하였다.

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

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

노드는 스스로를 이 두 모델을 사용해 가볍고 효율적이라고 표현했다.

그럼 이 모델이 무엇인지, 그리고 장단점으로는 어떤 것들이 있는지 알아보자.


2. 이벤트 기반

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

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

이벤트 기반 시스템에서는 특정 이벤트가 발생할 때 무엇을 할지 미리 등록해두어야 한다.

이것을 이벤트 리스너콜백(callback) 함수를 등록한다고 표현한다.

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

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

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

여러 이벤트가 동시에 발생했을 때 어떤 순서로 콜백 함수를 호출 스택(Call Stack)에 가져올지 이벤트 루프가 판단한다.

이벤트 루프에 대해서 이미 알고있겠지만, 중요한 개념이니 간략히만 설명하겠다.

노드는 자바스크립트 코드에서 맨 위부터 한 줄씩 실행한다.

함수 호출 부분을 발견했다면 호출한 함수를 호출 스택에 넣는다.

다음 코드가 콘솔에 어떤 로그를 남길지 예측해보자.

function first() {
  second();
  console.log('First');
}

function second() {
  third();
  console.log('Second');
}

function third() {
  console.log('Third');
}

first();

.
.
.

first 함수가 제일 먼저 호출되고, 그 안의 second 함수가 호출된 뒤, 마지막으로 third 함수가 호출된다.

호출된 순서와는 반대로 실행이 완료된다.

따라서 콘솔에는 "Third", "Second", "First" 순으로 찍히게 된다.

이를 쉽게 파악하는 방법은 호출 스택을 그려보는 것이다.

위 그림에서 anonymous는 자바스크립트 코드를 처음 실행 시의 전역 컨텍스트를 의미한다.

컨텍스트는 함수가 호출되었을 때 생성되는 환경을 의미한다.

자바스크립트는 실행 시 기본적으로 전역 컨텍스트 안에서 돌아간다고 생각하면 된다.

함수의 실행이 완료되면 호출 스택에서 지워진다.

third, second, first, anonymous 순으로 지워지고, anonymous 함수까지 실행이 모두 완료가 되었다면 호출 스택은 비어있게 된다.

이번에는 특정 밀리초 이후에 코드를 실행하는 setTimeout()을 second 함수에서 사용해보겠다.

콘솔에 어떤 로그가 기록될지 예측해보자.

function first() {
  second();
  console.log('First');
}

function second() {
  third();
  setTimeout(() => {
    console.log('Second');
  }, 3000);
}

function third() {
  console.log('Third');
}

first();

.
.
.

아까와는 달리 "Second"가 제일 마지막에 출력되었다.

이것은 console.log("Second")가 호출 스택에서 제일 마지막에 실행되고 지워졌다는 의미이다.

그렇다면 노드 내부에서 어떻게 동작하길래 이런식으로 출력되는 것일까?

콜백 함수가 호출 스택에 언제 어떻게 들어가는지 알기 위해서는 먼저 몇 가지 개념들을 알아야 한다.

  • 이벤트 루프: 이벤트 발생 시 호출할 콜백 함수들을 관리하고, 호출된 콜백 함수의 실행 순서를 결정하는 역할을 담당한다. 노드가 종료될 때까지 이벤트 처리를 위한 작업을 반복하므로 루프라고 불린다.
  • 태스크 큐: 호출되어야 할 콜백 함수들이 기다리는 공간이다.
  • 백그라운드: 타이머I/O 작업 콜백 또는 이벤트 리스너들이 대기하는 곳이다.

그럼 이제 과정을 살펴볼 차례인데 복잡하다면 복잡한 이 과정을 gif로 정리해놓은 사진이 있어 가져와보았다.

글로 이해하는 것보다 사진으로 이해하는 것이 훨씬 빠르기 때문에 아래의 사진과 설명으로도 충분히 이해할 수 있을 것이다.


function foo() {
  console.log('First');
}

function bar() {
  setTimeout(() => console.log('Second'), 500);
}

function baz() {
  console.log('Third');
}

bar();
foo();
baz();

  1. bar()를 호출해서 setTimeout()을 실행한다.

  2. setTimeout()안의 콜백은 WEB API(백그라운드)에 추가되고, setTimeout(), bar()는 호출 스택에서 지워진다.

  3. WEB API 안에서 방금 추가된 콜백이 실행되고, 그 동안 foo()가 호출되어 화면에 "First"가 출력된다.

  4. foo()는 return 하는 값이 없으니 undefined를 반환하고 호출스택에서 지워진다.

  5. baz()가 호출되고, 그동안 처리가 처리가 끝난 콜백이 큐에 추가된다.

  6. 호출된 baz()가 화면에 "Third"를 출력하고 이 역시 undefined를 반환하며 호출스택에서 지워진다. 현재 호출 스택은 비어있는 상태이다.

  7. 이벤트 루프는 호출 스택이 비어있는 것을 보고 큐에서 호출을 기다리고 있는 콜백이 있는지 확인한다. 큐에 기다리고 있던 콜백이 있으므로 이 콜백을 가져와서 호출 스택에 추가한다.

  8. 콜백이 실행되며 마지막으로 "Second"가 출력된다.


노드가 뒤에서 어떻게 동작하는지 어느정도 이해가 됐을 것이다.

하지만 무조건 위와 같이 실행되는 것은 아니다.

만약 호출 스택에 함수들이 너무 많이 차 있으면 콜백이 백그라운드에서 처리가 완료가 되어 큐로 이동되어 있더라도 바로 실행되지 않는다.

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


3. 논블로킹 I/O

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

오래 걸리는 함수를 백그라운드로 보내서 다음 코드가 먼저 실행되게 하고, 그 함수가 다시 태스크 큐를 거쳐 호출 스택으로 올라오기를 기다리는 방식이다.

이 방식이 논블로킹 방식이다.

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


4. 싱글 스레드

이벤트 기반, 논블로킹 모델과 더불어 노드를 설명할 때 자주 나오는 용어가 하나 더 있다.

바로 싱글 스레드이다.

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

작성한 자바스크립트 코드가 동시에 실행될 수 없는 이유이기도 하다.

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

프로세스와 스레드의 차이는 다음과 같다.

  • 프로세스
    • 운영체제에서 할당하는 작업의 단위
    • 노드나 웹브라우저 같은 프로그램은 개별적인 프로세스
    • 프로세스 간에는 메모리 등의 자원을 공유하지 않음
  • 스레드
    • 프로세스 내에서 실행되는 흐름의 단위
    • 프로세스는 스레드를 여러 개 생성해 여러 작업을 동시에 처리 가능
    • 스레드들은 부모 프로세스의 자원을 공유

노드가 싱글 스레드라는 말은 들어봤을 것이다.

하지만 엄밀히 말하자면 싱글 스레드로 동작하지는 않는다.

노드를 실행하면 먼저 프로세스가 하나 생성된다.

그리고 그 프로세스에서 스레드들을 생성하는데, 이때 내부적으로 스레드를 여러 개 생성한다.

그중에서 사람이 직접 제어할 수 있는 스레드는 하나뿐이다.

그래서 흔히 노드가 싱글 스레드라고 여겨지는 것이다.

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

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

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

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

언뜻 보면 여러 개의 일을 동시에 처리할 수 있기 때문에 멀티 스레드가 싱글 스레드보다 좋아 보이지만 꼭 그런 것만은 아니다.

이해를 돕기 위한 예시를 하나 들어보겠다.


한 음식점에 점원이 한 명밖에 없고 손님은 여러 명이다.

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

그 후 다음 손님의 주문을 받는다.

이런 구조라면 다음 손님은 이전 손님의 요리가 나올 때까지 아무것도 못 하고 기다리고 있어야 한다.

이것이 바로 싱글 스레드(점원), 블로킹 모델이다.

매우 비효율적이다.


이번에는 점원이 한 손님의 주문을 받고, 주방에 주문 내역을 넘긴 뒤 다음 손님의 주문을 받는다.

요리가 끝나길 기다리지 않고 주문이 들어왔다는 것만 알려주는 것이다.

주방에서 요리가 완료되면 완료된 순서대로 손님에게 서빙한다.

주문한 순서와 서빙하는 순서가 일치하지 않을 수도 있다.

이것이 싱글 스레드, 논블로킹 모델이다.

바로 노드가 채택하고 있는 방식이다.

점원은 한 명이지만 혼자서 많은 일을 처리할 수 있다.

하지만 그 점원 한명이 아파서 쓰러지거나 하면 큰 문제가 생길 수 있다.

또한 주문을 받거나 서빙을 하는 데 시간이 오래 걸린다면 주문이 많이 들어왔을 때 버거울 수 있다.


멀티 스레드 방식에서는 손님이 올 때마다 점원이 한 명씩 맡아 주문을 받고 서빙한다.

언뜻 보면 싱글 스레드보다 좋은 방법인 것 같지만 장단점이 있다.

일단 손님 한 명당 점원도 한 명이면 서빙 자체는 걱정이 없다.

점원 한 명에게 문제가 생겨도 다른 점원으로 대체하면 되기 때문이다.

하지만 손님의 수가 늘어날수록 점원의 수도 늘어난다.

손님 수가 줄어들었을 때 일을 하지 않고 노는 점원이 있다는 것도 문제가 된다.

점원을 새로 고용하거나 기존 직원을 해고하는 데는 비용이 발생한다.


그렇다면 점원 여러 명(멀티 스레드)이 모두 논 블로킹 방식으로 주문을 받으면 더 좋지 않을까 하는 의문이 들 수 있다.

실제로 그렇다.

다만 멀티 스레드 방식으로 프로그래밍하는 것은 상당히 어려우므로 멀티 프로세싱 방식을 대신 사용한다.

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


5. 서버로서의 노드

노드는 기본적으로 싱글 스레드, 논 블로킹 모델을 사용하므로 노드 서버 또한 동일한 모델일 수밖에 없다.

따라서 노드 서버의 장단점은 싱글 스레드, 논 블로킹 모델의 장단점과 크게 다르지 않다.

서버에는 기본적으로 I/O 요청이 많이 발생하므로, I/O 처리를 잘하는 노드를 서버로 사용하면 좋다.

노드는 libuv 라이브러리를 사용하여 I/O 작업을 논 블로킹 방식으로 처리한다.

따라서 스레드 하나가 많은 수의 I/O를 혼자서도 감당할 수 있다.

하지만 노드는 CPU 부하가 큰 작업에는 적합하지 않다.

우리가 작성하는 코드는 모두 스레드 하나에서 처리된다.

코드가 CPU 연산을 많이 요구하면 스레드 하나가 혼자서 감당하기 어렵다.

이와 같은 특성을 활요하려면 노드를 어디에 사용해야 할까?

개수는 많지만 크기는 작은 데이터를 실시간으로 주고받는 데 적합하다.

네트워크데이터베이스,디스크 작업 같은 I/O에 특화되어 있기 때문이다.

실시간 채팅 애플리케이션이나 주식 차트, JSON 데이터를 제공하는 API 서버가 노드를 많이 사용한다.


노드에는 웹 서버가 내장되어 있어 입문자가 쉽게 접근할 수 있다.

노드 외의 서버를 개발하다 보면 아파치(Apache), nginx, IIS처럼 별도의 웹 서버를 설치하는 경우도 있다.

심지어 톰캣(Tomcat) 같은 웹 애플리케이션 서버(WAS)를 추가로 설치하는 경우도 있다.

이 경우에는 프로그래밍 외에도 웹 서버와 WAS 사용법을 익혀야 한다.

하지만 노드는 내장된 웹 서버를 사용하면 되므로 편리하다.

하지만 나중에 서버 규모가 커지면 결국 nginx 등의 웹 서버를 노드 서버와 연결해야만 한다.


6. 서버 외의 노드

처음에는 노드를 대부분 서버로 사용했지만, 노드는 자바스크립트 런타임이므로 용도가 서버에만 한정되지 않는다.

사용 범위가 점점 늘어나서 노드는 웹, 모바일, 데스크톱 애플리케이션 개발에도 사용된다.

profile
Backend Engineer

0개의 댓글