Node.js?
서버, 프레임워크, 프로그래밍 언어 과연 셋 중 Node.js는 어디에 속할까?
사실 그 어디에도 속하지 않는다..
(내가 Node.js가 뭔지에 대해 알고 싶었던 이유도 여러 개념들 중 뭐가 Node.js인지 궁금해서였었다..)
사실 Node.js는 "크롬 V8 자바스크립트 엔진"으로 빌드된, 자바스크립트를 실행할 수 있는 RunTime 환경(특정 언어로 만든 프로그램들을 실행할 수 있는 환경)이다.
Node.js는 크롬 자바스크립트 엔진인 "V8"과 비동기 작업을 처리하는 "libuv library" 를 기반으로 이루어져 있는데 간략히 설명하면 아래와 같다.
V8
- Chrome 브라우저 용 JS엔진으로써, 혁신적인 설계와 속도 그리고 효율적인 메모리 관리로 높은 평가 받음
libuv library
- C언어로 만들어져서 낮은 수준의 기능들을 JS에 맵핑하고 사용하도록 해주는 바인딩 세트
- Event-driven, Non-blocking I/O Model 구현
Express?
웹 및 모바일 애플리케이션을 위한 Node.js웹 애플리케이션 프레임워크
- HTTP 요청에 대해 라우팅 및 미들웨어 기능 제공
- 노드 패키지 매니저(NPM)을 사용해 수천만 개의 재사용 가능한 패키지에 접근 가능
Node.js의 특징
1. Non-blocking I/O
논블로킹(Non-blocking)이란 이전 작업이 완료될 때까지 멈추지 않고 다음 작업을 수행하는 것을 뜻한다.
컴퓨터의 실행 상태 프로세스는 크게 3가지로 나누어진다.
1. 실행 (running) : 명령어가 실행되는 상태, 즉 프로세스가 프로세서를 점유한 상태.
2. 대기 또는 분류 (waiting) : 프로세서가 이벤트 (입출력 종료와 같은 외부 신호)가 일어나길 기다리는 상태.
3. 준비 (ready) : 프로세스가 프로세서를 할당받기 위해 기다리는 상태. cpu만 할당되면 바로 일할 수 있는 상태.
컴퓨터에서 어떤 프로세스가 실행 될 때, 프로세스는 CPU의 자원을 사용하고 I/O 작업을 진행한다. 그동안 CPU는 I/O 작업이 끝나길 기다리는데 이때 기다림 없이 다음 명령어를 수행함으로써, 즉 CPU가 쉬지 않고 일하게 함으로써 시간적 이득을 보게 되는 것이다.
- Blocking : 호출된 함수 A가 자신의 작업이 모두 끝날 때까지 제어권을 가지고 있어 호출한 함수 B를 대기시킴(그동안 프로그램 처리 진행 불가)
- Non-Blocking : 호출된 함수가 바로 다음 호출한 함수에게 제어권을 주어 다음 작업을 바로 수행 가능
2. Single Thread
프로세스(Process)
- 컴퓨터에서 연속적으로 실행되고 있는 컴퓨터 프로그램
- 메모리에 올라와 실행되고 있는 프로그램 인스턴스(독립적인 개체)
- 동적인 의미로는 실행된 프로그램
- 프로세스 간 메모리 등 자원 공유를 하지 않음
스레드(Thread)
- 프로세스 내에서 실행되는 여러 흐름의 단위
- 프로세스가 할당받은 자원을 이용하는 실행의 단위
- 스레드는 한 프로세스 내에서 동작되는 여러 실행의 흐름으로 프로세스 내의 주소 공간이나 자원들을 같은 프로세스 내에 스레드끼리 공유하면서 실행
- 같은 프로세스 안에 있는 여러 스레드들은 같은 힙 공간을 공유한다. 반면에 프로세스는 다른 프로세스 메모리에 직접 접근할 수 없음
자 그럼 Node.js는?
1. Node.js 실행 시 프로세스 하나 생성
2. 생성된 프로세스는 여러 개의 쓰레드 생성 -> 잉? 여러개의 스레드면 Multi-Thread 아니야? Node.js는 Single Thread라며
3. 하지만! 제어 가능한 스레드는 단 하나! -> Node.js가 Single Thread인 이유
한 줄 요약
즉, Node.js는 Single Thread & Non-Blocking으로써 한 명의 요리사가 있고 그 요리사 혼자 주문을 받는데 주문에 대한 요리가 나오기 전이어도 다른 주문을 받을 수 있다! 하지만 주문 순서대로 일을 처리하며 한 번에 하나의 요리만 조리한다!
3. 이벤트 기반(Event Driven)
이벤트가 발생할 때 미리 지정해 놓은 작업을 수행하는 방식을 말한다.
Node.js는 Event Listener에 Callback 함수를 지정해서 동작하는데, 이벤트 기반 모델에서는 이벤트 루프(Event Loop)라는 개념이 등장한다. 단순한 개념만 보면 이벤트 루프를 이해하기 어렵기 때문에 흐름을 살펴보려고 한다.
JS의 엔진은 두 가지 주요 구성요소인 Memory Heap과 Call Stack으로 이루어져 있다.
- Memory Heap: 객체는 힙, 대부분 구조화되지 않은 메모리 영역에 할당됨(변수 & 객체에 대한 모든 메모리 할당이 이루어지는 곳)
- Call Stack: 코드가 실행될 때 콜 스택이 쌓임(실행 순서를 기억하며, 현재 뭘 하는지 알 수 있음)
JS 또한 Single Thread 프로그래밍 언어이기 때문에 하나의 콜 스택이 존재하고, 따라서 한 번에 한 가지의 일만 처리할 수 있다.
function multiply(a,b){
return a * b;
}
function square(n){
multiply(n,n);
}
function printSquare(){
var squared = square(n);
console.log(squared);
}
printSquare(4);
이 코드는 콜 스택에 어떻게 기록이 될까?
함수를 실행하려면 스택에 해당하는 함수를 집어넣게 되는데, 함수에서 return이 일어나면 스택의 가장 위쪽에서 해당 함수를 꺼내게 된다. 따라서 위 코드의 경우 다음과 같은 순서로 동작한다.
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
multiply(n, n) | ||||||||
square(n) | square(n) | square(n) | console.log(squared) | |||||
printSquare(4) | printSquare(4) | printSquare(4) | printSquare(4) | printSquare(4) | printSquare(4) | printSquare(4) | ||
main() | main() | main() | main() | main() | main() | main() | main() |
순서를 나열하자면 이렇지만 어느 정도 코드를 짜 본 사람이라면 저 코드가 어떻게 실행되는지 머릿속에 그려졌을 것이다.
바로 그게 콜 스택에서 일어나는 일이다.
자 근데 동기적으로 실행되는 JS에서 한 개의 콜 스택만 가지고 있으면, 한 가지 함수가 엄청 느릴 경우 다른 함수의 실행까지 영향을 미칠 것이 너무 뻔하다.
마냥 기다려야 할까?
비동기 콜백을 사용하면 이러한 문제를 해결할 수 있다. 그게 뭔지 알기 위해 다음의 코드를 한 번 살펴보자.
console.log('Hi');
setTimeout(function cb() {
console.log('There');
}, 5000);
console.log('JSConfEU');
실행 결과는 예상한 대로 'Hi'가 찍히고 'JSConfEU'가 찍히고 5초 뒤에 There가 찍힌다.
그럼 콜 스택에서는 어떨까? 다음과 같은 순서로 push가 되고 pop 된다.
1 | 2 | 3 | 4 |
console.log('Hi') | setTimeout(cb, 5000) | console.log('JSConfEU') | |
main() | main() | main() | console.log('there') |
만약 동기적으로 실행이 되어야 한다면 2단계에서 setTimeOut(cb, 5000);이 결과를 줄 때까지 스택에서 사라지면 안 될 것이다.
하지만 사라졌다가 마법같이 4단계에서 재등장한다.
분명 JS는 한 번에 한 가지 일만 처리할 수 있다고 했다. 근데 어떻게 한가지 코드가 끝나기 전에 다른 코드를 처리하고 다시 등장할 수 있는 걸까?
답은 Background에서 찾아볼 수 있다.
Node는 JS의 싱글 스레드에서 효과적으로 동작할 수 있는 C++ API를 제공한다(브라우저의 경우 Web API를 제공).
이러한 API가 Background에서 도와주기 때문에 한 번에 한 가지 일만 처리할 수 있는 JS가 동시성을 가질 수 있는 것이다.
V8엔진에 있는 것이 아니라 JS가 실행되는 런타임 환경에 존재하는 별도의 API이다!
동시성 및 비동기 콜백의 동작은 다음과 같은 구조 속에서 이루어진다.
- Call Stack
- API가 속해있는 BackGround
- Callback Queue(task queue)
- Event Loop
아까 그 코드는 구조 속에서 다음과 같은 실행 과정을 거친다.
1. setTimeout()은 2단계가 끝나고 pop 되면서 API(Background)로 일을 전가하고 API에서 5초를 세는 역할을 수행해준다
2. 여기서 도움을 주는 API는 어떤 작업이 수행 중일 때 Stack에 갑자기 끼어드는 것은 불가능하다. 그렇기 때문에 Background에서 일이 끝나면 API는 Callback Queue에 콜백 함수를 넘겨준다.
3. Event Loop는 기회를 살피다가 Call Stack이 비어있을 때! Callback Queue에서 콜백 함수를 가져와 Call Stack에 전달해준다.
비어있어야 한다는 조건이 있기 때문에 setTimeout(cb, 0) 이더라도 출력 결과는 setTimeout(cb, 5000)와 똑같다.
4. Call Stack에 cb라는 콜백 함수가 들어가고 그 안에 있는 코드인 console.log('there')가 스택에 쌓인다.
5. 실행되고 콜 스택은 비게 된다.
역할 정리
Background(APIs)
- 타이머나 이벤트 리스너들이 대기하는 곳
- 여러 작업이 동시에 실행될 수 있음
Callback Queue(콜백 큐)
- 이벤트 발생 후, 백그라운드에서 Callback Queue로 콜백 함수를 보냄
- Callback Queue에 있는 콜백 함수들은 Call Stack이 비워지기를 기다림
Event Loop(이벤트 루프)
- Callback Queue의 콜백 함수들을 Call Stack에 전달
- 단, Call Stack이 비어있을 때만 가져옴
비동기 콜백? 논-블로킹? 똑같은 거 아니야?
글을 다 읽고 나면 이런 의문이 생긴다. 그래서 찾아보았는데 비슷하지만 다른 개념이다.
논-블로킹 I/O는 처리가 완료되지 않으면 에러를 회신하고, 블록 상태로 만들지 않는 반면 비동기 I/O는 처리를 바로 할 수 없을 때. 처리가 완료되는 시점까지 백그라운드에서 대기하고, 종료한 타이밍을 회신하는 차이가 있다.
참고 자료
- SOPT 27기 서버 세미나 자료
- 윤자이 기술 블로그
- 왜 node.js는 single-thread인가
- Philip Roberts 강연(JSConf)-추천