Node.js의 등장
Node.js는 자바스크립트 런타임입니다. Node.js가 등장하기 전에는 자바스크립트는 브라우저 환경에서만 구동될 수 있었지만 Node.js의 등장 이후 자바스크립트를 컴퓨터에서 실행할 수 있게 되었습니다. 2008년 구글이 V8엔진을 사용하여 크롬을 출시했고, V8 엔진을 통해 자바스크립트의 속도 문제가 해결되었습니다. 따라서 2009년에 라이언 달이 V8 엔진 기반의 노드 프로젝트를 시작하며 Node.js가 등장했습니다. Node.js는 V8과 더불어 libuv라는 C와 C++로 구현된 I/O 라이브러리로 구성되어 있습니다. 덕분에 Node.js는 비동기적이며 빠른 실행 환경을 제공할 수 있게 되었습니다.
Node.js의 동작 원리
1. 이벤트 기반
이벤트 기반 시스템에서는 특정 이벤트가 발생할 때 어떤 동작을 발생시킬지 미리 등록해두어야합니다. 이것이 바로 이벤트 리스너에 콜백 함수를 등록하는 것입니다.
이벤트 기반 시스템은 이벤트 루프, 콜스택, 테스크 큐, 백그라운드를 조합하여 동작합니다. 먼저, 각각의 구성요소가 하는 역할을 간단하게 짚고 넘어가겠습니다.
- 이벤트 루프: 이벤트 발생 시 콜백 큐에서 어떤 콜백 함수를 호출할지 관리합니다. 호출된 콜백 함수의 실행 순서를 결정하는 역할을 담당합니다. 프로그램 종료시까지 이벤트 처리를 위해 계속 반복하기 때문에 루프라고 부릅니다.
- 백그라운드: setTimeout같은 타이머나 이벤트 리스너들이 대기하는 곳입니다. 이벤트가 발생하면 이곳에 있던 타이머나 콜백 함수가 테스크 큐로 보내집니다.
- 테스크 큐: 이벤트가 발생하면 이벤트 루프가 테스크 큐로 이벤트나 타이머의 콜백 함수를 보냅니다. 정해진 순서대로 콜백들이 줄을 서있고, 큐 자료구조처럼 맨 앞에 있는 콜백이 콜스택으로 보내지기 때문에 콜백 큐라고도 불립니다. 하지만 콜백들의 순서가 바뀌는 경우도 있습니다.
- 콜스택: 현재 실행 중인 함수의 호출 정보를 저장하는 공간입니다. Javascript 엔진이 함수를 실행하면 콜스택에 함수 호출 정보가 추가되고, 함수 실행이 완료되면 해당 정보가 제거됩니다.
위의 구성요소들이 어떻게 이벤트 기반 시스템을 동작하게 하는지 아래 예시를 통해 이벤트 방식이 어떻게 동작 하는지 살펴보겠습니다.
1-1. Node.js의 이벤트 시스템 동작 과정
이 코드를 실행하면 console.log
가 어떤 순서로 출력될까요?
로그의 내용으로도 유추할 수 있겠지만 “안녕” → “잘가” → “사실 아직 안갔지롱~”순서로 출력됩니다. 내부적으로 어떤 동작을 해서 다음과 같은 로그가 출력되는지 gif를 통해 살펴보겠습니다.
- 먼저 전역 컨텍스트인
anonymous
가 호출 스택에 들어갑니다. console.log(”안녕”)
이 호출 스택에 들어갑니다.- 호출 스택의 맨 위에 있는
console.log(”안녕”)
이 호출되고 “안녕”이라는 로그가 찍히게 됩니다. setTimeout(run,1000)
이 호출 스택에 들어갑니다.setTimeout(run,1000)
이 호출되면 타이머와 함께run
콜백을 백그라운드로 보냅니다.- 백그라운드 영역에서 1초를 세는 동안
console.log(”잘가”)
가 호출 스택에 들어갔다가 pop되면서 실행되고, “잘가”라는 로그가 찍히게 됩니다. - 백그라운드에서 1초를 센 후
run
함수를 태스크 큐로 보냅니다. - 이벤트 루프는 호출 스택이 비어있으면 태스트 큐에 있는
run
함수를 꺼내 호출 스택에 올립니다. run
함수가 호출되며 스택에서 꺼내지고 “사실 아직 안갔지롱~”이라는 로그가 출력됩니다.
이벤트 루프는 호출 스택이 비어 있을 때만 태스크 큐에 있는 함수를 호출 스택으로 가져오기 때문에 호출 스택에 너무 많은 함수들이 있으면 타이머 시간이 모두 지난 후에도 콜백함수가 실행되지 않을 수 있습니다. 그래서 setTimeout의 시간이 정확하지 않은 경우가 발생할 수 있습니다.
1-2. Javascript 브라우저 환경에서 이벤트 방식의 동작 과정
Node.js 환경과 다르게 브라우저 환경에서는 이벤트 방식이 어떻게 동작하는지 다음 예시를 통해 살펴보도록 하겠습니다.
위의 코드가 실행되면 클릭 이벤트 리스너에 console.log("hello")
를 출력하는 콜백 함수가 등록됩니다. 그리고 브라우저 클릭 이벤트가 발생하면 Call Stack, Web Apis, Callback
Queue에 다음과 같은 일이 일어납니다.
📌 맨 왼쪽 위부터 화살표 방향대로 진행됩니다.
- 프로그램을 실행하면 전체 코드가 콜스택에 등록됩니다.
- 콜스택의 코드를 해석해서 클릭 이벤트 리스너에 콜백함수를 등록합니다.
- 버튼이 클릭되는 순간 callback queue에
anoymous
가 등록됩니다.anoymous
는 처음 실행 시의 전역 컨텍스트를 의미합니다. 자바스크립트 코드는 실행되는 순간 전역 컨텍스트 안에서 동작한다고 생각해주세요. - 이벤트 루프에 의해 callback queue에 있던 전역 컨텍스트가 콜스택으로 옮겨지고, 클릭 이벤트 리스너의 콜백함수 안에 있던
console.log
도 콜스택 영역으로 push됩니다. - 콜스택 맨 위에 있는 동작부터 pop되면서 코드가 실행됩니다.
2. 논 블로킹 I/O
- 논 블로킹이랑 이전 작업이 완료되는 것을 기다리지 않고 다음 작업을 수행하거나, 여러 작업을 동시에 처리하는 것을 의미합니다.
- 블로킹은 이전 작업이 끝나야만 다음 작업을 수행하는 것을 의미합니다.
- I/O는 입력/출력 을 의미합니다. Javascript의 I/O에는 파일 시스템 접근, 네트워크 요청 등이 있습니다.
Node.js에서는 I/O 작업을 백그라운드로 넘겨서 동시에 처리합니다. 따라서 동시에 처리될 수 있는 작업은 최대한 묶어서 백그라운드로 넘겨야 시간을 절약할 수 있습니다.
❓이쯤되면 생기는 궁금증!!
Javascript는 싱글 스레드 언어인데 백그라운드의 I/O 작업은 어떻게 동시에 실행될까요? 만약 백그라운드의 작업이 동시에 실행된다면, setTimeout등을 통해 우리가 작성한 함수를 콜백으로 넘기면 동시에 실행될까요?
우리가 작성한 함수는 절대로 동시에 실행될 수 없습니다. 왜냐하면 Javascript는 싱글 스레드 언어이기 때문입니다. 그럼 여기서 또 의문점이 생깁니다. 백그라운드 영역에 타이머가 여러개이거나, 네트워크 요청이 여러개인 경우 모두 동시에 실행되기 때문입니다. 어떻게 이것이 가능할까요?
바로 libuv라는 Nodejs의 코어 라이브러리 덕분입니다. libuv에서 타이머, 파일 I/O, 네트워크 요청 등을 비동기적으로 처리합니다. 이 작업들은 백그라운드에서 병렬로 처리되고, 작업이 완료되면 해당 작업의 콜백 함수가 실행됩니다.
그렇다면 우리는 왜 setImmediate같은 것을 사용해서 콜백 함수를 등록하는 걸까요? 어차피 동시에 실행될 수도 없는데… 그것은 바로 작업의 순서를 변경해주기 위해서 입니다. 작업의 순서를 순서를 변경함으로써 엄청나게 오래걸리는 작업 때문에 간단한 작업들이 대기하는 상황을 막을 수 있습니다.
3. 싱글 스레드
Node.js는 싱글 스레드, 논 블로킹 방식으로 동작합니다. 싱글 스레드를 이해하기 위해서 프로세스와 스레드가 무엇인지에 대해 살펴보겠습니다.
프로세스
프로세스는 운영체제에 의해 실행되는 독립적인 작업 단위입니다. (비공식적으로는 프로세스는 실행중인 프로그램입니다) 프로세스의 메모리 배치는 일반적으로 스택, 힙, 데이터, 텍스트 영역으로 구분되어있습니다. 각 프로세스는 독립된 주소 공간을 할당받아서 자신만의 자원을 사용합니다. 때문에 한 프로세스에 문제가 생겨도 다른 프로세스에 영향을 미치지 않습니다. 이러한 프로세스의 특성으로 안정성과 격리성이 높지만 프로세스 간의 전환 및 자원 공유에는 오버헤드가 발생합니다.
스레드
스레드는 프로세스 내에서 실행되는 작은 작업의 단위입니다. 하나의 프로세스 안에 여러개의 스레드를 가질 수 있습니다. 스레드는 같은 프로세스에 속한 다른 스레드와 코드, 데이터 섹션, 열린 파일이나 신호와 같은 운영체제 자원들을 공유합니다.
노드 프로세스에는 스레드가 하나뿐일까?
Node.js를 실행하면 프로세스가 하나 생성됩니다. 그리고 그 프로세스에서 스레드들을 생성하는데 하나가 아닌 여러 개의 스레드를 생성합니다. 하지만 그 중에서 우리가 직접 제어할 수 있는 스레드는 하나뿐입니다. 이것이Node.js를 싱글 스레드라고 부르는 이유입니다.
싱글 스레드, 논 블로킹
Node.js는 싱글 스레드 방식입니다. 따라서 Node.js는 동시에 하나의 작업만 처리할 수 있습니다. 하지만 위에서 설명한 논 블로킹 방식으로 인해 비동기 프로그래밍을 가능하게 하고, 동시성을 구현할 수 있습니다.
참고 문서
- https://nodejs.org/ko/docs/guides/event-loop-timers-and-nexttick
- https://nodejs.org/ko/docs/guides/blocking-vs-non-blocking
- http://docs.libuv.org/en/v1.x/design.html
- http://latentflip.com/loupe/
- https://codingjuny.tistory.com/58
- 조현영, 『Node.js 교과서』, 길벗, p29-42
- Abraham Silberschatz,Peter Baer Galvin,Greg Gagne, 『운영체제』, p118-119, p176-177