await/ async에 대한 문법 소개 글을 작성하다가 기초가 부족해서 이 질문까지 왔다.
await / async 설명 작성 중.. → “아.. 이걸 설명하려니까 Promise를 배워야겠다.” → “어 Promise를 보면 비동기 처리가 가능하다고 하는데 자바스크립트는 싱글스레드가 아닌가?“ → “Node.js는 다른가? Node.js는 JS의 단순 런타임이 아닌가?”
JS가 싱글 스레드인데 어떻게 비동기 처리가 가능한지
학생 분들이 계신 방에 질문해봤다.
→ 저학년들이 많고, Node 진형 개발자분들이 적어 스레드 풀 얘기까지는 나왔지만, 제대로 된 답이 나오지 않았다.
현직자 분들이 많은 개발바닥 채팅방에 질문해봤다.
→ Node.js에 포함된 libuv라는 라이브러리가 비동기 처리를 책임져준다.
이번 글에서는 내가 조사하고 오해하고 있었던 부분에 대해서 글을 적어보려고한다.
오해했던 내용들
Node.js == JavaScript, But Node.js !== JavaScript
Node.js는 JavaScript의 런타임이다. 라는 말이 너무 유명해서 Node.js는 JS 그 자체인줄 알았다.
https://nodejs.org/en/docs/meta/topics/dependencies/
위 링크를 보면, Node.js가 종속성을 가지고 있는 라이브러리의 목록이 나온다.
간단히 요약하면 이렇다.
- V8
오픈소스 JS 엔진입니다. 브라우저 밖에서 자바스크립트가 실행될 수 있도록 합니다.
구글이 개발하여 크롬에서 사용중입니다.
문서 - libuv
비동기 작업을 위하여 스레드 풀, 논 블로킹 I/O 작업을 담당하는 라이브러리입니다.
문서 - llhttp
http 구문 분석
문서 - c-ares
일부 비동기 DNS 요청
문서 - OpenSSL
보안 관리
문서 - zlib
파일 압축 및 압축 해제
문서
여기서 중요한 건 V8 엔진과 libuv이에요.
V8과 libuv에 대해서는 뒤에 설명하겠습니다. 지금은 일단 그런게 있구나 하고 넘어가주세요.
Node.js는 순수한 싱글스레드인가?
결론부터 말하자면 Node.js는 싱글스레드가 아니다. 하지만 javascript는 싱글스레드다.
Node.js의 작동 원리
Node.js의 내부 구조
Node.js의 내부 구조를 크게 나누어보자면,
- Node.js의 내장 라이브러리(llhttp, c-ares, OpenSSL, zlib)
- V8 엔진
- libuv
으로 나뉜다.
여기서 libuv가 Node.js의 특징인 I/O, 비동기 처리, 이벤트 기반을 담당하고 있기 때문에 libuv에 대해서 깊게 이해해야만 Node.js를 이해할 수 있습니다.
블로킹? 논 블로킹?
블로킹과 논 블로킹 개념을 이해하기에는 아래 목차인 이벤트 루프를 같이 보셔야하기 때문에 이해가 안되시면 같이 보셔야할 것입니다.
그 전에 Node.js에서 I/O라는 키워드가 언급되는데, 그놈의 I/O가 뭔지 이해하고 갑시다.
I/O란, libuv가 지원하는 작업(http, Database와 통신, third party API, 파일 I/O ) 등 과의 상호작용( Input / Output )입니다.
블로킹(Blocking)
블로킹이란 Node.js 프로세스에서 새로운 작업(Task) 처리를 위해 현재 진행중인 작업이 완료 될때까지 기다려야만 하는 상황입니다.
block: (지나가지 못하게) 차단됨
Node.js에서는 libux가 모든 I/O 메서드가 논 블로킹인 비동기 방식을 제공하고 콜백 함수를 받습니다.
코드 예시를 봅시다.
파일 I/O를 사용하여 동기로 파일을 읽는 예제입니다.
const fs = require('fs');
const data = fs.readFileSync('/file.md'); // 파일을 읽을 때까지 여기서 블로킹됩니다.
console.log(data);
moreWork(); // console.log 이후 실행될 것입니다.
이를 비동기로 작성하면 이렇게 됩니다.
const fs = require('fs');
fs.readFile('/file.md', (err, data) => {
if (err) throw err;
console.log(data);
});
moreWork(); // console.log 이전에 실행될 것입니다.
첫 예제가 두번째보다 간단해 보이지만 두 번째 줄에서는 전체 파일을 다 읽는 동안 블로킹된다는 단점이 있습니다. 동기 예제에서는 오류가 발생하면 반드시 처리해주어야 하고 그렇지 않으면 프로세스가 죽을 것입니다. 비동기 예제에서는 예제에 나왔듯이 에러를 던질지 아닐지는 작성자에게 달려있습니다.
논 블로킹(Non-Blocking)
논 블로킹은 위와 같은 예제로 비동기 상황에 블로킹이 일어나지 않는 상황을 이야기합니다.
Node.js에서는 논 블로킹 싱글스레드를 지원합니다.
Node.js에서 비동기 코드를 만났을 때 어떻게 논 블로킹으로 처리하는지는 이벤트 루프 탭에서 보겠습니다.
동기 & 비동기, 블로킹 & 논 블로킹의 에제를 봅시다.
동기 + 블로킹
동기 + 블로킹은 코드에서 작업을 하다가 새로운 한가지 작업을 시작하면 완료될 때까지 기다렸다가 다음 작업을 진행합니다.
동기 + 논 블로킹
동기 + 논 블로킹 방식은 작업을 OS에게 떠넘깁니다.
데이터베이스의 작업이 진행되면서 꾸준히 완료 여부를 확인하고, 완료되었다면 결과를 받아와서 작업을 진행합니다. 블로킹되지 않기 때문에 비동기처럼 동작이 가능합니다.
비동기 + 논 블로킹
비동기 + 논 블로킹은 가장 효율이 좋은 방식입니다. 자신의 작업이 멈추지 않으며 다른 작업도 가능합니다.
이벤트 루프란?
이벤트 루프는 시스템 커널에 작업(Task)를 떠넘겨서 Node.js가 논 블로킹 I/O 작업을 수행하도록 해줍니다.(JavaScript가 싱글 스레드임에도 비동기가 가능한 이유입니다.)
대부분의 현재 커널은 멀티 스레드이므로 백그라운드 환경에서 다수의 작업을 실행할 수 있습니다.
Node.js에서 커널로 작업을 떠넘기고, 커널에서 작업 중 하나가 완료된다면 Node.js에게 알려주어 적절한 콜백을 Poll 큐에 추가하여 실행합니다.
Browser Event Loop VS Node.js Event Loop Browser와 Node.js는 다른 구현체를 사용합니다. Browser(Chrome, V8): libevent Node.js: libuv
이벤트 루프 단계
Polling 이란? 하나의 장치가 충돌 회피 또는 동기화 처리 목적으로 다른 장치의 상태를 주기적으로 검사하여 일정한 조건을 만족할 때 송수신 등의 자료처리를 하는 방식
이벤트 루프 처리 방식
Node.js를 시작하면 제공된 입력 스크립트(JavaScript 코드)에 있는 작업을 하나씩 추가합니다.
- timers(종료 시간 입력):
setTimeout()
과setInterval()
같은 타이머 함수들을 처리합니다. 이벤트 루프가 각 단계들을 순회하면서 timer 단계에 오면 처리할 수 있는 함수들을 콜백함수로 실행합니다. 스케쥴링한 콜백을 실행합니다. - pending callbacks(대기 중인 콜백 함수들):
다음 루프 반복으로 연기된 I/O 콜백을 큐에 담습니다. - idle, prepare:
내장에서 처리되기 때문에 스킵해도됩니다. - poll:
poll 단계는 두 가지 주요 기능을 가집니다.- I/O를 얼마나 오래 블록(block)하고 폴링해야 하는지 계산합니다. 그 다음
- poll 큐에 있는 이벤트를 처리합니다.
이벤트 루프가 poll 단계에 진입하고 스케쥴링 된 타이머가 없을 때, 둘 중 하나의 상황이 발생합니다.- poll 큐가 비어있지 않다면, 이벤트 루프가 콜백의 큐를 순회하면서 큐를 다 소진하거나 시스템 하드 제한에 도달할 때까지 동기로 콜백을 실행합니다.
- poll 큐가 비어있다면, 다음 중 두가지가 진행됩니다.
- 스크립트가
setImmediate()
에 의해 스케줄링된 경우, 이벤트 루프는 poll 단계를 종료하고, 예약된 스크립트를 처리하기 위해 check 단계로 넘어갑니다. - 스크립트가
setImmediate()
에 의해 스케줄링되지 않은 경우, 이벤트 루프는 콜백이 대기열에 추가될 때까지 기다린 후 즉시 실행합니다.
- 스크립트가
- check(확인):
setImmediate()
의 콜백함수가 실행됩니다. 만약 이벤트 루프가 poll 단계에서 작업을 수행한 후 비어있게 된다면, poll 단계에서 이벤트를 기대리지 않고 check 단계로 넘어갑니다. - close callbacks(콜백 완료하기):
소켓이나 핸들이 갑자기 닫힌 경우(예:soket.destory()
), 이 단계에서‘close’
이벤트를 발생시킵니다.
레퍼런스
- 블로킹과 논 블로킹 살펴보기 | Node.js Docs https://nodejs.org/ko/docs/guides/blocking-vs-non-blocking/
- Node.js 이벤트 루프, 타이머, process.nextTick() | Node.js Docs https://nodejs.org/ko/docs/guides/event-loop-timers-and-nexttick/#the-node-js-event-loop-timers-and-process-nexttick
- Node.js 동작원리 | vincent https://medium.com/@vdongbin/node-js-동작원리-single-thread-event-driven-non-blocking-i-o-event-loop-ce97e58a8e21
- 동기/비동기와 블로킹/논 블로킹 | DevEric https://deveric.tistory.com/99
- Browser VS Node Event Loop 차이 | yoeubi https://yoeubi28.medium.com/browser-vs-node-event-loop-차이-9c87f858051d
- [10분 테코톡] 멍토의 Blocking vs Non-Blocking, Sync vs Async | 멍토 https://youtu.be/oEIoqGd-Sns
'트러블 슈팅' 카테고리의 다른 글
팀원이 설정한 알람이 내 폰에서 울려야한다. (0) | 2023.03.28 |
---|---|
어떨 때 composite key를 사용해야 할까? (0) | 2023.03.19 |
OAuth 로그인을 하는데 새로운 계정이 생성되면 안된다. (0) | 2023.02.23 |
[NestJS] AdminJS를 이용한 크롤러 관리 패널 제작기 (0) | 2023.01.14 |
[puppeteer] 크롤러 성능향상, 이미지 수신 차단 (0) | 2023.01.02 |
글 내용 중 잘못되거나 이해되지 않는 부분은 댓글을 달아주세요! 감사합니다! 문의: puleugo@gmail.com