7월에 새 백엔드 팀에 합류했다.
이 팀의 기술 스택은 Express로 하나의 B2C 서비스를 운영 중이었고,
NestJS로 B2B 서비스를 기획 및 개발 진행 중이었다.
백엔드는 개인 프로젝트에서 PHP만 다뤄보았기 때문에 Node 계열을 제대로 다뤄보는 것은 이번이 처음이었다.
업무 투입에 앞서 Node.js의 개념과 특징을 공부했었는데, 노션에 정리했던 글들을 이제부터 블로그에 포스팅해보려 한다.
Node.js 란?
Node.js는 Chrome V8 Javascript 엔진으로 빌드 된 JavaScript 런타임이다.
크롬 V8 엔진이란 웹 브라우저를 만드는 데 기반을 제공하는 오픈 소스 자바스크립트 엔진을 의미하며,
런타임이란 특정 언어로 개발된 프로그램을 해석하고 실행할 수 있는 환경, 즉 프로그래밍언어(자바스크립트)가 구동(해석하고 실행) 되는 환경이다. (언어가 아니다.)
Node.js가 등장하기 전의 자바스크립트는 브라우저 안에서만 동작했었다.
2008년 구글이 크롬 브라우저를 출시하면서 V8 엔진을 개발, 이로 인해 자바스크립트의 실행 속도가 대폭 개선되었다.
공식 홈페이지에서 소개를 통해 노드는 언어나 프레임워크가 아닌 런타임 이란 사실을 알게 되었다.
Node.js 사용이유?
Node.js는 JavaScript를 사용하기 위해 만들어진 것이기 때문이다.
JavaScript는 C/C++, Java 와 같은 프로그래밍 언어이다.
하지만 이름에서 알 수 있듯 JavaScript는 독립적인 언어가 아닌 스크립트 언어이다.
스크립트 언어는 특정한 프로그램 안에서 동작하는 프로그램이기 때문에 웹 브라우저 프로그램 안에서만 동작을 한다.
즉, 웹 브라우저(크롬, 사파리, 익스플로러, 파이어폭스 등)가 없으면 사용할 수 없는 프로그램이다.
여기서 Node.js가 나오는 이유가 된다.
즉, JavaScript를 웹 브라우저에서 독립시킨 것으로 Node.js를 설치하게 되면 터미널 프로그램(윈도우의 cmd, 맥의 terminal 등)에서 Node.js를 입력하여 브라우저 없이 바로 실행할 수 있다.
이렇게 Node.js를 이용하여 웹 브라우저와 무관한 프로그램을 만들 수 있게 되었다.
Node.js 의 구조 및 동작원리
노드는 V8과 더불어 libuv라는 라이브러리를 사용한다. V8과 libuv는 C와 C++로 구현되어 있으며,
자바스크립트 코드는 노드를 V8과 libuv에 연결해 준다.
여기서 libuv 라이브러리는 노드의 특성인 이벤트 기반, 논 블로킹 I/O 모델을 구현한다.
Node.js는 Google의 Chrome V8 자바스크립트 엔진을 기본으로 동작한다.
Single Thread 기반의 Event Loop (libuv)가 돌면서 요청을 처리하며, 시스템적으로 Non-Blocking IO를 지원하지 않는 IO 호출이 있는 경우, 이를 비동기 처리하기 위해서 내부의 Thread pool을 별도 이용하여 처리한다.
그 위에 네트워크 프로토콜을 처리하는 socket, http 바인딩 모듈이 로드 되고, 맨 윗단에, node.js에서 제공하는 standard library (파일 핸들링, console 등)이 로드 된다.
Node.js 의 특징
- 이벤트 루프
- 싱글스레드
- 논블로킹 I/O
이벤트 루프(Event Loop)
- 이벤트 발생 시 호출할 콜백 함수들을 관리
- 호출된 콜백 함수의 실행 순서를 결정
- 노드가 실행 종료될 때까지 이벤트 처리를 위한 작업을 반복(loop)
노드는 이벤트 기반으로 클릭이나 네트워크 요청 등 특정 이벤트가 발생할 때 무엇을 할지 미리 등록해두고, 이를 이벤트 리스너에 콜백 함수를 등록한다.
이후 이벤트가 발생하면 리스너에 등록해둔 콜백 함수를 호출하며, 이벤트가 끝난 후 노드는 다음 이벤트가 발생할 때까지 대기한다.
노드는 하나의 스레드로 동작하지만 I/O 작업이 발생한 경우 이를 비동기적으로 처리할 수 있다.
분명 하나의 스레드는 하나의 실행 흐름만을 가지고 있고 파일 읽기와 같이 기다려야 하는 작업을 실행하면 그 작업이 끝나기 전에는 아무것도 할 수 없어야만 한다.
그러나 노드는 하나의 스레드만으로 여러 비동기 작업들을 블로킹 없이 수행할 수 있고 그 기반에는 이벤트 루프가 존재한다.
이벤트 루프란 여러 이벤트가 동시에 발생했을 때 어떤 순서로 콜백 함수를 호출할지를 이벤트 루프가 판단한다.
노드는 이벤트가 종료될 때까지 이벤트 처리를 위한 작업을 반복하므로 루프라고 부른다.
이벤트 루프의 동작 원리는 call stack과 callback queue를 지속적으로 감시(busy-wating) 하면서 call stack이 빈 경우 callback queue에 작업이 있는지 확인하고 이를 call stack으로 옮겨오는 역할을 한다.
싱글 스레드(Single Thread)
Node.js는 싱글스레드, 논 블로킹 모델로 싱글 스레드가 혼자서 일을 처리하지만 들어오는 요청 순서가 아닌 논 블로킹 방식으로 이전 작업이 완료될 때까지 대기하지 않고 다음 작업을 수행한다.
싱글 스레드란 스레드가 하나뿐이라는 것을 의미한다. (= 동시에 하나의 작업만을 처리할 수 있다.)
노드를 실행하면 프로세스가 하나 생성되는데 그 프로세스에서 스레드를 생성할 때 내부적으로는 스레드를 여러 개 생성한다. 하지만 직접 제어가 가능한 것은 스레드 하나뿐이기 때문에 노드가 싱글 스레드라고 하는 것이다.
하나의 스레드만 제어가 가능하므로 많은 요청이 오면 하나씩 처리하고 논 블로킹 방식을 이용하여 대기 시간을 줄인다.
논블로킹(Non-blocking) I/O
- 이벤트 루프를 잘 활용해 오래 걸리는 작업을 효율적으로 처리할 수 있다.
- 동시에 실행될 수 있는 작업과 동시에 실행될 수 없는 작업이 있다.
- 특히 파일 시스템 접근, 네트워크를 통한 요청 작업은 입력(Input)/출력(Output)의 일종이며, 이러한 작업을 할 때 노드는 비동기 방식으로 블로킹을 만들지 않게 끔(논 블로킹) 처리한다.
- 함수 호출 시 바로 실행하는 것이 아니라(동기→블로킹) 일단 어느 곳에 쌓아 놓고 동시에 요청을 처리하고(비동기→논 블로킹) 요청이 완료된 순서대로처리(스택 이용) 한다.
이벤트 루프를 활용하면 작업 시간이 긴 것도 효율적으로 처리할 수 있는데, 작업에는 동시에 실행 가능한 작업과 그렇지 않은 작업이 있다. 기본적으로 자바스크립트 상에서 돌아가는 것은 동시에 실행될 수 없지만 I/O 작업은 동시에 처리가 가능하다. 이때, 논 블로킹 방식으로 처리하는 것이 노드이다.
논 블로킹으로 수행하기 위해서는 작업들이 모두 동시에 처리될 수 있는 작업이어야 한다. 노드는 이러한 I/O 작업들을 백그라운드에서 동시에 처리하여 시간을 절약한다. 이러한 작업 순서는 성능을 크게 좌우한다.
정리하자면 Node.js에서의 논 블로킹 I/O 모델은 블로킹 작업(Input, Output과 관련된 작업 / http, Database CRUD, third party api, filesystem)들을 백그라운드(libuv의 스레드 풀)에서 수행하고, 이를 비동기 콜백 함수로 이벤트 루프에 전달하는 것을 말한다.
여기까지가 노드의 특징 3가지인데,
마지막 특징인 논블로킹 I/O를 공부하다보니 블로킹, 논블로킹, 동기, 비동기 라는 단어들이 정리가 되지않아, 개념과 차이점을 찾아보았다.
Blocking(블로킹)과 Non-blocking(논블로킹)
블로킹과 논블로킹은 A 함수가 B 함수를 호출했을 때, 제어권을 어떻게 처리하느냐에 따라 달라진다.
블로킹은
1. A 함수가 B 함수를 호출하면, 제어권을 A가 호출한 B 함수에 넘겨준다.
2. 제어권을 넘겨받은 B는 함수를 실행하고, A는 B에게 제어권을 넘겨주었기 때문에 함수 실행을 잠시 멈춘다.
3. B함수는 실행이 끝나면 자신을 호출한 A에게 제어권을 돌려준다.
논블로킹은 A함수가 B함수를 호출해도 제어권은 그대로 자신이 가지고 있는다.
1. A함수가 B함수를 호출하면, B 함수는 실행되지만, 제어권은 A 함수가 그대로 가지고 있는다.
2. A함수는 계속 제어권을 가지고 있기 때문에 B함수를 호출한 이후에도 자신의 코드를 계속 실행한다.
코드 예시를 보면 좀 더 직관적으로 이해가 될 것이다.
//블로킹 예시
const fs = require('fs');
const data = fs.readFileSync('/file.md'); //파일을 읽을 때까지 여기서 블로킹 된다.
console.log(data);
nextWork(); //nextWork는 console.log(data)가 실행된 후 실행된다.
//논 블로킹 예시
const fs = require('fs');
fs.readFile('/file.md', (err, data) => {
if (err) throw err;
console.log(data)
});
nextWork(); //nextWork는 console.log(data) 이전에 실행될 것이다.
위의 블로킹 예시에서는 file.md를 다 읽을 때까지 전체 JavaScript 시스템이 멈추지만,
아래 논블로킹 예시에서는 비동기적으로 실행되기에 전체 JavaScript 시스템이 멈추지 않고 newxtWork()가 먼저 실행된다. 그리고 file.md를 다 읽고 나서 fs.readFile의 두 번째 파라미터로 들어간 콜백(callback) 함수가 실행된다.
여기서 콜백 함수란 어떤 이벤트(이 경우에서는 file I/O)가 끝나고 나서 실행되는 함수를 말한다.
Synchronous(동기)와 Asynchronous(비동기)
다음은 동기와 비동기에 대해 설명하겠다.
동기와 비동기의 차이는 호출되는 함수의 작업 완료 여부를 신경쓰는지의 여부의 차이다.
동기는 함수 A가 함수 B를 호출한 뒤, 함수 B의 리턴값을 계속 확인하면서 신경쓰는 것이 동기이다.
비동기는 함수 A가 함수 B를 호출할 때 콜백 함수를 함께 전달해서, 함수 B의 작업이 완료되면 함께 보낸 콜백 함수를 실행한다.
함수 A는 함수 B를 호출한 후로 함수 B의 작업 완료 여부에는 신경쓰지 않는것이 비동기이다.
정리하자면 블로킹 논블로킹은 제어의 관점이라고 할 수 있고, 동기 비동기는 순서와 결과(처리)의 관점이라고 할 수 있습니다.
사실 이렇게 글로만 읽으면 너무 추상적이고 쉽게 와닿지가 않는다.
그래서 나는 일상 생활의 예시로 이해했다.
- 상사가 보고서를 읽을 때까지 사원이 기다리는 것 -> 블로킹
- 서류를 상사에게 전달하고 서류 읽는 동안 사원은 돌아가서 할 일 하는 것 -> 논 블로킹
- 상사가 서류를 읽는 동안 사원은 할 일 하면서 계속 다 읽었냐 물어보는 것 -> 동기
- 상사가 서류를 읽고 결과를 메일로 보내면 사원은 읽든지 말든지 관심 없고 나중에 처리하는 것 -> 비동기
7월에 공부했던 내용들을 거의 5개월이 지난 후에 다시 읽고 정리해보면서 그때는 추상적으로 와닿았던 부분들이
조금 더 이해되었다.