Node.js 2009년 Ryan Dahl에 의해 만들어진 기술이다. C/C++과  JavaScript로 이루어져 있으며, 기본적으로 JavaScript 코드를 브라우저 밖에서 구동한다. Node.js에 대해 설명하기 이전에 왜 Ryan은 JavaScript를 사용하였는지,그리고 JavaScript가 어떻게 브라우저 밖에서 구동되는지 설명하고자 한다. 이를 이해하기 위해서는 Chrome V8, Non-Blocking I/0, Event Loop에 대해 알아야 한다. 


Chrome V8 Engine

Google에서 만든 JavaScript 컴파일러라고 생각하면 편하다. 실제 Chrome에서도 사용하고 있는 기술이며, 이로인해 Chrome에서 JavaScript 구동 속도가 빨라졌다. V8을 사용하여 JavaScript 코드를 효율적인 Machine code로 변환이 가능하며, 메모리 관리와 garbage collection 또한 해준다. 

V8 Engine이 어떻게 JavaScript를 최적화 시켜주는지 이해하는 것이 중요하다. 이것을 이해하고 V8 Engine 최적화 방법에 따라 코드를 짜는 것이 좋은 코드라고 할 수 있다. 

- Hidden Classes

p1과 p2는 같은 hidden class를 가지지만, p2에 z가 추가 되면 서로 다른 hidden class를 갖기 때문에 성능에 악영향을 끼친다. 

      • 모든 객체 멤버를 생성자 함수 안에서 초기화 (나중에 변화시키지 않도록 하자)
      • 항상 같은 순서로 멤버를 초기화

- Numbers

V8에서는 사용자가 사용하는 값을 통해서 Number 타입을 추론한다. 데이터 타입은 동적으로 변할 수 있기 때문에, 효율적으로 값을 나타내는 태그 사용이 필요하고, number 타입을 지속적으로 쓰는 것이 중요하다. 

      • 31비트 부호있는 정수를 사용


- Normal Arrays

큰 배열을 처리하기 위해서는 '키 값이 순서대로 채워지신 선형 저장소'인 Fast Element와 '그렇지 않은 해쉬 테이블 저장소'인 Dictionary Element가 존재한다. 배열 저장소가 한 유형에서 다른 유형으로 변경되지 않는 것 또한 중요하다.

      • 배열은 초기화 시킨다.
      • 숫자 배열의 요소를 활용한다.
      • Index는 0부터 시작하는 연속키를 사용한다.


- Double Arrays

상단부 코드 경우 a[2]가 선언될 때에 double 배열의 형태로 바뀌었다가, 다시 a[3]가 선언될 때에 일반 배열로 바뀐다. 이렇듯 선언 될때마다 배열의 종류가 바뀌지 않도록 신경써야한다.



- JavaScript Compile

2가지 종류의 컴파일러를 가지고 있다. 이는 따로 설명하지 않겠다. 

      • Full Compiler
      • Optimize Compiler



Non-Blocking I/O

대다수의 프로그래밍 언어는 Blocking I/O 시스템을 가지고 있다. Blocking 시스템이란 A가 수행 중일 때 B는 A가 다 마무리 된 이후에 수행됨을 의미한다. 즉, C언어 코드를 생각해보았을 때 순서대로 코드가 실행되는 것을 생각하면 된다. 

인터넷 창은 어떠한가? 우리가 흔히 들어가는 Naver의 메인 화면만 보아도 광고 동영상이 나옴과 동시에, 뉴스를 확인 할 수 있고, 실시간 검색어를 클릭 할 수 있다. 즉, 동시에 여러가지 일들을 진행할 수 있다. Node.js 기반의 웹 서버에서도 동시에 여러 HTTP 요청을 처리하기도 한다. 

아래는 Non-Blocking I/O를 그림으로 나타낸 것이다. 왼쪽은 순차적으로 진행되는 Blocking I/O이고, 오른쪽이 Non-Blocking I/O이다. 




Event Loop

위에서 웹 서비스는 Non-Blocking I/O 시스템을 사용해야한다고 하였다. 하지만 브라우저를 비롯한 JavaScript는 단일 스레드 기반의 언어이다. 스레드가 하나라는 말은 곧 동시에 하나의 작업만 처리할 수 있다는 의미이다. 이 문제를 해결하기 위해 등장하는 개념이 Event Loop이다. 자바스크립트는 Event Loop를 이용하여 비동기 방식으로 동시성을 지원한다. 이와 같은 동시성은 JavaScript 엔진이 아닌 브라우저나 Node.js가 담당한다. 


Browser-Event

(출처: http://meetup.toast.com/posts/89)


브라우저 환경에 관한 그림이다. 비동기 호출을 위해 사용되는 'Timer'를 비롯하여 Event Loop 및 Task Queue 또한 JavaScript 엔진이 아닌 외부에 있음을 볼 수 있다. 다음은 Node.js 환경에 관한 그림이다. 


NodeJS


Node.js는 비동기 I/O를 지원하기 위해 LIBUV 라이브러리를 사용하며, Event Loop를 제공한다. JavaScript 엔진은 비동기 작업을 위해 Node.js API를 호출하며, 이때 넘겨진 콜백은 Event Loop를 통해 계획되고 실행된다. 


Task Queue는 콜백 함수들이 대기하는 큐(First In First Out) 형태의 배열이다. Event Loop는 호출 스택이 비워질 때 마다 Task Queue에서 콜백 함수를 꺼내와서 실행하는 역할을 해준다. 이런 식으로 Event Loop는 '현재 실행중인 태스크가 있는지''태스크 큐에 태스크가 있는지'를 반복적으로 확인하는 것이다. 간단히 확인하면 다음과 같다. 

    • 모든 비동기 API들은 작업이 완료되면 콜백 함수를 태스크 큐에 추가한다. 

    • Event Loop는 '현재 실행중인 태스크가 없을 때' 태스크 큐의 첫 번째 태스크를 꺼내와 실행한다. 

예제를 확인해보고 마무리하도록 하자. 

이 코드를 실행하면 아무런 지연없이  setTimeout  함수가 세번 호출된 이후에 실행을 마치고 호출 스택이 비워질 것이다. 그리고 10ms가 지나는 순간  foo  bar  baz  함수가 순차적으로 태스크 큐에 추가된다. Event Loop는  foo , 함수가 태스크 큐에 들어오자 마자, 호출 스택이 비어있으므로 바로  foo 를 실행해서 호출 스택에 추가한다.  foo  함수의 실행이 끝나고 호출 스택이 비워지면 이벤트 루프가 다시 큐에서 콜백인  bar 를 가져와 실행한다.  bar 의 실행이 끝나면 마찬가지로 큐에 남아있는  baz 를 큐에서 가져와 실행한다. 그리고  baz 까지 실행이 모두 완료되면 현재 진행중인 태스크도 없고 태스크 큐도 비어있기 때문에, 이벤트 루프는 새로운 태스크가 태스크 큐에 추가될 때까지 대기하게 된다.

이렇듯 Event Loop를 사용하여 Non-Blocking I/O 시스템을 구현한다면 우리는 CPU cycle과 RAM을 더욱 효율적으로 활용할 수 있을 것이다. 

참고

콜백 예시

다음에는 Framework을 비롯하여 MVC등에 대해 알아보겠다. 


+ Recent posts