Node.js(이하 노드)가 무엇이고 어디에 쓰이며 누가 쓰는지를 알아보고, 노드의 핵심 개념을 알아보자!
목차
1.1. 핵심 개념 이해하기
노드가 무엇인지에 대해 여러 가지 의견이 많지만, 어떠한 설명도 노드 공식 사이트의 설명보다 정확하지는 않을 것이다.
노드 공식 사이트에서는 노드를 다음과 같이 설명하고 있다.
Run JavaScript Everywhere
(어디서나 Node.js를 실행하세요.)
왜 노드 공식 사이트에서는 노드를 어디서나 실행하라고 할까?
노드를 배우는 대부분의 사람들은 서버로 사용하는 방법을 익히기 위해 공부하고 있을 것인데, 노드는 사실 서버를 실행하는 것 외에도 많은 곳에서 쓰일 수 있다고 한다.
1.1.1. 서버
그럼 서버란 무엇이며, 어떤 역할을 할까?
그림 1-1. 클라인언트와 서버
서버는 네트워크를 통해 클라이언트에 정보나 서비스를 제공하는 컴퓨터 또는 프로그램을 말한다.
클라이언트는 요청을 보내는 주체로 브라우저일 수도 있고, 데스크톱 프로그램일 수도 있고, 모바일 앱일 수도 있고, 다른 서버에 요청을 보내는 서버일 수도 있다.
우리가 어떤 서비스에서 아이디와 패스워드를 입력하면, 브라우저는 해당 서비스의 서버 위치를 파악하고 데이터를 그 컴퓨터로 전송(요청)하게 된다. 그러면 서버는 요청을 처리한 후 데이터를 브라우저(클라이언트)로 반환(응답)하여 사용자에게 보여준다. 이러한 과정을 수행하는 컴퓨터가 바로 서버이다.
노드는 자바스크립트 프로그램이 서버로서 기능하기 위한 도구를 제공하므로 서버 역할을 수행할 수 있다. 그렇다면 왜 다른 언어를 사용하지 않고 굳이 노드를 사용해 서버를 만들까? 이 궁금증을 해결하려면 먼저 노드의 특성을 알아야 한다.
1.1.2. 자바스크립트 런타임
노드는 Chrome V8 JavaScript 엔진으로 빌드된 자바스크립트 런타임입니다.
노드는 자바스크립트 런타임이다.
런타임은 특정 언어로 만든 프로그램들을 실행할 수 있는 환경을 뜻한다.
따라서 노드는 자바스크립트 프로그램을 컴퓨터에서 실행할 수 있다.
기존에는 자바스크립트 프로그램을 웹 브라우저 위에서만 실행할 수 있었다.
브라우저는 자바스크립트 런타임을 내장하고 있으므로 자바스크립트 코드를 실행할 수 있다.
브라우저 외의 환경에서 자바스크립트를 실행하기 위한 여러 시도가 있었으나, 자바스크립트의 실행 속도 문제 때문에 대부분이 큰 호응을 얻지 못했다.
하지만 2008년 구글이 V8 엔진을 사용해 크롬을 출시하자 이야기가 달라졌다.
당시 V8 엔진은 다른 자바스크립트 엔진과는 달리 매우 빨랐고, 오픈 소스로 코드를 공개했다.
속도 문제가 해결되자 라이언 달(Ryan Dahl)은 2009년 V8 엔진 기반의 노드 프로젝트를 시작했다.
그림 1-2. 노드의 내부 구조
노드는 V8과 더불어 libuv라는 라이브러리를 사용한다. V8과 libuv는 C와 C++로 구현되어 있다.
우리가 코딩한 자바스크립트 코드는 노드가 알아서 V8과 libuv에 연결해주므로, 노드를 사용할 때는 C와 C++은 몰라도 된다.
libuv 라이브러리는 노드의 특성인 이벤트 기반, 논블로킹 I/O 모델을 구현하고 있다. 이 모델이 무엇인지, 장단점으로는 어떤 것들이 있는지 알아보자.
1.1.3. 이벤트 기반
이벤트 기반(event-driven)이란 이벤트가 발생할 때 미리 지정해둔 작업을 수행하는 방식을 의미한다. 이벤트로는 클릭이나 네트워크 요청 등이 있을 수 있다.
그림 1-3. 이벤트 기반
이벤트 기반 시스템에서는 특정 이벤트가 발생할 때 무엇을 할지 미리 등록해둬야 한다. 이를 이벤트 리스너(event lisener)에 콜백(callback) 함수를 등록한다고 표현한다. 버튼을 클릭할 때 경고창을 띄우도록 설정하는 것을 예로 들어보자. 클릭 이벤트 리스너에 경고창을 띄우는 콜백 함수를 등록해두면 클릭 이벤트가 발생할 때마다 콜백 함수가 실행돼 경고창이 뜨는 것이다.
노드도 이벤트 기반 방식으로 동작하므로, 이벤트가 발생하면 이벤트 리스너에 등록해둔 콜백 함수를 호출한다. 발생한 이벤트가 없거나 발생했던 이벤트를 다 처리하면, 노드는 다음 이벤트가 발생할 때까지 대기한다.
이벤트 루프
이벤트 기반 모델에서는 이벤트 루프(event loop)라는 개념이 등장한다. 여러 이벤트가 동시에 발생했을 때 어떤 순서로 콜백 함수를 호출할지를 이벤트 루프가 판단한다.
노드는 자바스크립트의 코드의 맨 위부터 한 줄씩 실행한다.
함수 호출 부분을 발견했다면 호출한 함수를 호출 스택(call stack)에 넣는다.
다음 코드가 콘솔에 어떤 로그를 남길지 예측해보자.
function first() {
second();
console.log('첫 번째');
}
function second() {
third();
console.log('두 번째);
}
function third() {
console.log('세 번째');
}
first();
JavaScript
복사
first 함수 → second 함수 → third 함수 순으로 호출이 되고, 호출된 순서와는 반대로 실행이 완료된다.
이를 쉽게 파악하는 방법은 호출 스택을 그려보는 것이다.
그림 1.4. 호출 스택
그림 1.4에서 anonymous 함수는 처음 실행 시의 전역 컨텍스트(global context)를 의미한다.
콘텍스트는 함수가 호출되었을 때 생성되는 환경을 의미한다.
자바스크립트 코드는 실행 시 기본적으로 전역 콘텍스트 안에서 돌아간다고 생각하는 게 좋다.
함수는 실행되는 동안 호출 스택에 머물러 있다가 실행이 완료되면 호출 스택에서 지워진다.
third → second → first → anonymous 순으로 지워지고, anonymous 콘텍스트까지 실행이 모두 완료되었다면 호출 스택은 비어 있게 된다.
따라서 콘솔의 출력 결과는 다음과 같다.
세 번째
두 번째
첫 번째
JavaScript
복사
이번에는 특정 밀리초(1,000분의 1초) 이후에 코드를 실행하는 setTimeout을 사용한다.
콘솔에 어떤 로그가 기록될지 예측해보자.
function run() {
console.log("3초 후 실행");
}
console.log("시작");
setTimeout(run, 3000);
console.log("끝");
JavaScript
복사
정답은 다음과 같다.
시작
끝
3초 후 실행
JavaScript
복사
아마 일부 사람들은 ‘시작 → 3초 후 실행 → 끝’ 으로 잘못 예측했을 것이다.
이를 이해하기 위해서는 이벤트 루프, 태스크 큐(task queue), 백그라운드(background)를 알아야 한다.
•
이벤트 루프
◦
이벤트 발생 시 호출할 콜백 함수들을 관리
◦
호출된 콜백 함수의 실행 순서를 결정하는 역할
◦
노드가 종료될 때까지 이벤트 처리를 위한 작업을 반복하므로 루프(loop)라고 부른다.
•
백그라운드
◦
setTimeout 같은 타이머나 이벤트 리스너들이 대기하는 곳
◦
여러 작업이 동시에 실행될 수 있다.
•
태스크 큐
◦
이벤트 발생 후, 백그라운드에서는 태스크 큐로 타이머나 이벤트 리스너의 콜백함수를 보낸다.
◦
정해진 순서대로 콜백들이 줄을 서 있으므로 콜백 큐라고도 한다.
◦
콜백들은 보통 완료된 순서대로 줄을 서 있지만, 특정한 경우 순서가 바뀌기도 한다.
그림 1.5는 코드가 실행되는 내부 과정을 묘사한 그림이다.
그림 1.5. 이벤트 루프
그림이 이해가 잘 되지 않는다면, 글로 동작 과정을 세세하게 살펴보자.
1.
전역 콘텍스트인 anonymous가 호출 스택에 들어간다.
2.
setTImeout이 호출 스택에 들어간다.
3.
setTimeout이 실행된다.
4.
타이머와 함께 run 콜백을 백그라운드로 보낸다.
5.
그와 동시에, setTimeout이 호출 스택에서 빠진다.
6.
anonymous가 호출 스택에서 빠진다.
7.
백그라운드에서는 3초를 센 후 run 함수를 태스크 큐로 보낸다.
8.
호출 스택이 비어있으므로, 이벤트 루프는 태스크 큐에서 run 함수를 가져와 호출 스택에 넣고 실행한다.
9.
실행 완료 후 run 함수를 호출 스택에서 제거한다.
10.
이벤트 루프는 태스크 큐에 콜백함수가 들어올 때까지 계속 대기한다.
Q. settimeout을 0.1초로 설정하면, 정확히 0.1초 뒤에 실행되는가?
정답은 No.
호출 스택에 함수가 너무 많이 들어오면 0.1초가 지난 후에도 run 함수가 실행되지 않을 수 있다. 왜냐하면, 이벤트 루프는 호출 스택이 비어 있을 때만 태스크 큐에 있는 run 함수를 호출 스택으로 가져오기 때문이다.
1.1.4. 논블로킹 I/O
이벤트 루프를 잘 활용하면 오래 걸리는 작업을 효율적으로 처리할 수 있다.
작업은 크게 동시에 실행될 수 있는 작업과 동시에 실행될 수 없는 작업으로 나뉜다.
우리가 작성한 자바스크립트 코드는 동시에 실행될 수 없고, I/O 작업 같은 것은 동시에 처리될 수 있다. 파일 시스템 접근(파일 읽기 및 쓰기, 폴더 만들기 등)이나 네트워크를 통한 요청 같은 작업이 I/O의 일종이다. 이러한 작업을 할 때 노드는 논블로킹 방식으로 처리할 수 있다.
논블로킹(non-blocking)이란 이전 작업이 완료될 때까지 대기하지 않고 다음 작업을 수행하는 것을 의미한다. 반대로 블로킹(blocking)은 이전 작업이 끝나야만 다른 작업을 수행하는 것을 의미한다.
그림 1.6. 블로킹 vs 논블로킹
그림 1.6을 보면 작업들이 모두 동시에 처리될 수 있는 작업이라는 전제하에, 블로킹 방식보다 논블로킹 방식이 같은 작업을 더 짧은 시간에 처리한다는 것을 알 수 있다.
노드는 I/O 작업을 백그라운드로 넘겨 동시에 처리하곤 한다. 따라서 동시에 처리될 수 있는 작업들은 최대한 묶어서 백그라운드로 넘겨야 시간을 절약할 수 있다.
다음 예제는 블로킹 방식의 코드이다. 콘솔 결과를 예측해보자.
function longRunningTask() {
// 오래 걸리는 작업
console.log('작업 끝');
}
console.log('시작');
longRunningTask();
console.log('다음 작업');
JavaScript
복사
결과는 다음과 같다.
시작
작업 끝
다음 작업
JavaScript
복사
작업을 수행하는 데 오래 걸리는 longRunningTask 함수가 있고, 이 함수가 블로킹 방식의 I/O 작업을 한다고 생각해보자. 이 작업이 완료되기 전까지는 이어지는 console.log(’다음 작업’)이 호출되지 않는다.
이번에는 setTimeout을 사용해서 코드를 바꿔보자.
function longRunningTask() {
// 오래 걸리는 작업
console.log('작업 끝');
}
console.log('시작');
setTimeout(longRunningTask, 0);
console.log('다음 작업');
JavaScript
복사
결과는 다음과 같다.
시작
다음 작업
작업 끝
JavaScript
복사
이벤트 루프를 이해했다면, setTimeout의 콜백 함수인 longRunningTask가 태스크 큐로 보내지므로 순서대로 실행되지 않는다는 것을 알 수 있다. 다음 작업이 먼저 실행된 후, 오래 걸리는 작업이 완료된다.
논블로킹 방식은 여러 요청을 동시에 처리할 수 있어 효율적인 경우가 많으나, 비동기 로직 처리와 에러 핸들링 등 복잡성 증가라는 단점도 있으니 상황에 맞게 적절히 사용해야 한다.
1.1.5. 싱글 스레드
이벤트 기반, 논블로킹 모델과 더불어 노드를 설명할 때 자주 나오는 용어가 싱글 스레드이다.
싱글 스레드란 스레드가 하나뿐이라는 것을 의미한다. 우리가 작성한 자바스크립트 코드가 동시에 실행될 수 없는 이유이기도 하다. 스레드를 이해하기 위해서는 프로세스부터 알아야 한다. 프로세스와 스레드의 차이는 다음과 같다.
그림 1.7. 스레드 vs 프로세스
•
프로세스
◦
운영체제에서 할당하는 작업의 단위
◦
노드나 웹 브라우저 같은 프로그램은 개별적인 프로세스
◦
프로세스 간에는 메모리 등의 자원을 공유하지 않는다.
•
스레드
◦
프로세스 내에서 실행되는 흐름의 단위
◦
프로세스는 스레드를 여러 개 생성해 여러 작업을 동시에 처리
◦
스레드들은 부모 프로세스의 자원을 공유하므로, 같은 주소의 메모리에 접근하여 데이터를 공유
노드는 싱글 스레드 방식을 채택하고 있다. 하지만 엄밀히 말하면 싱글 스레드로 동작하지는 않는다.
노드를 실행하면 먼저 프로세스가 하나 생성되고, 내부적으로 스레드 여러 개가 생성된다.
하지만, 그중에서 우리가 직접 제어할 수 있는 스레드는 하나뿐이다. 그래서 흔히 노드가 싱글 스레드라고 여겨지는 것이다.
이해를 돕기 위한 예시를 들어보자.
그림 1.8. 싱글 스레드, 블로킹 예시
한 음식점에 점원이 한 명 있고, 손님은 여러 명이 있다.
점원 한 명이 주문을 받아 주방에 넘기고, 주방에서 요리가 나오면 손님에게 서빙을 한다. 그 후 다음 손님의 주문을 받는다.
이런 구조라면 다음 손님은 이전 손님의 요리가 나올 때까지 아무 것도 하지 못하고 기다리고 있어야 한다. 이것이 바로 싱글 스레드(점원), 블로킹 모델이다. 매우 비효율적이다.
그림 1.9. 싱글 스레드, 논블로킹 예시
이번에는 점원이 한 손님의 주문을 받고, 주방에 주문 내역을 넘긴 뒤 다른 손님의 주문을 받는다. 요리가 끝나기까지 기다리는 대신, 주문이 들어올 때마다 주방에 알려주고 주방에서 요리가 완료되면 완료된 순서대로 손님에게 서빙한다. 요리의 특성(블로킹 or 논블로킹)에 따라 완료되는 순서가 다를 수 있으므로, 주문이 들어온 순서와 서빙하는 순서가 일치하지 않을 수 있다. 이것이 바로 노드가 채택하고 있는 방식인 싱글 스레드, 논블로킹 모델이다.
이 방식의 장점은 점원 혼자서 많은 일을 처리할 수 있다. 단점으로는 점원이 아파서 쓰러지거나 하면 큰 문제가 발생할 수 있다. 또한, 식당에 방문하는 고객들이 많아지거나 하면 점원 혼자서 버거울 수 있다.
그림 1.10. 멀티 스레드, 논블로킹
이를 해결하려면, 점원의 숫자를 늘려야한다. 그렇게 된다면 점원 여러 명(멀티 스레드)이 고객과 1대1 로 상호작용하게 된다. 이는 처리 능력을 크게 향상시키지만, 동시에 자원 관리와 동기화 문제를 야기할 수 있다. 노드는 이러한 복잡성을 피하고 단순성을 유지하기 위해 싱글 스레드 모델을 채택했지만, 클러스터링이나 워커 스레드와 같은 기능을 통해 멀티 코어 시스템의 이점을 활용할 수 있게 해준다.
1.2. 서버로서의 노드
그렇다면 노드를 서버로 사용할 때의 특성과 장단점에는 무엇이 있을까?
서버에는 기본적으로 I/O 요청이 많이 발생하므로, I/O 처리를 잘하는 노드를 서버로 사용하면 좋다.
하지만, 노드는 싱글 스레드이기에 CPU 부하가 큰 작업에는 적합하지 않다.
이와 같은 특성을 활용하면, 노드는 개수는 많지만 크기는 작은 데이터를 실시간으로 주고받는 데 적합하다. 네트워크나 데이터베이스, 디스크 작업 같은 I/O에 특화되어 있기 때문이다. 실시간 채팅 애플리케이션이나 주식 차트, JSON 데이터를 제공하는 API 서버가 노드를 많이 사용한다.
또한, 자바스크립트를 언어로 사용한다는 것도 큰 장점이다. 웹 브라우저도 자바스크립트를 사용하므로 서버까지 노드를 사용하면 하나의 언어로 웹 사이트를 개발할 수 있다. 이는 개발 생산성을 획기적으로 높였고, 생산성이 중요한 기업이 노드를 채택하는 이유가 되었다.
장점 | 단점 |
멀티 스레드 방식에 비해 적은 컴퓨터 자원 사용 | 기본적으로 싱글 스레드라서 CPU 코어를 하나만 사용 |
I/O 작업이 많은 서버로 적합 | CPU 작업이 많은 서버로는 부적합 |
멀티 스레드 방식보다 쉬움 | 하나뿐인 스레드가 멈추지 않도록 관리 필요 |
웹 서버가 내장되어 있음 | 서버 규모가 커졌을 때 서버를 관리하기 어려움 |
자바스크립트를 사용함 | 어중간한 성능 |
JSON 형식과 쉽게 호환됨 |
Reference
다음 글