기술자료

DBMS, DB 구축 절차, 빅데이터 기술 칼럼, 사례연구 및 세미나 자료를 소개합니다.

Node.js와 Node Binding 라이브러리 분석

기술자료
DBMS별 분류
Etc
작성자
dataonair
작성일
2014-04-09 00:00
조회
4633



Node.js의 내부 계층구조 파헤치기

Node.js와 Node Binding 라이브러리 분석



Node.js는 소프트웨어적으로 다양한 발전을 이어오며 여러 사례에 적합한 서버 플랫폼으로 그 모습을 갖춰왔다. 이렇게 다양한 기술의 지원을 가능케 하고 상대적으로 언어적인 확장이 쉬워 보이는 것은 NPM(Node Package Manager) 커뮤니티의 발전과도 연관 있지만, 그보다 Node.js를 구성하고 있는 주요 내부 계층과 라이브러리의 활약()이 있었기에 가능하다고 할 수 있다. 이번 연재에서는 Node.js의 내부 계층구조에 대해 분석해보고 어떠한 특징이 있는지 알아본다.



Node.js를 조금이라도 공부해 봤다면 <그림 1>의 다이어그램은 낯설지 않다. 기본적으로 Node.js 언어를 사용하는 개발자와 사용자에게는 <그림 1>에서 맨 위 계층으로 명시돼 있는 Node Standard Library가 가장 익숙할 것이다. 말 그대로 이 계층은 모든 기본 Node.js 라이브러리를 지니고 있다. HTTP, TCP, FS, OS, EVENT 등과 같이 아주 기본적인 라이브러리의 계층이다. Node Standard Library의 하위 계층에 속하는 Node Binding, V8과 같은 라이브러리들은 상위 계층 구조의 성격에 비하면 굉장히 시스템적이다. 이번 연재에서는 Node Standard Library의 하위 계층인 Node Binding에 대해 분석해보려 한다. V8 엔진과의 연동도 살펴보고 실제 Node Standard Library에서의 함수가 Node.js의 계층 구조에서 호출되는 과정도 알아본다. V8 엔진과 libuv 등에 대한 더 자세한 구조 분석은 다음 시간에 다룰 예정이니 참고하길 바란다.

tech_img1210.jpg

이벤트 루프 기반의 비동기 I/O

전통적인 프로그래밍 방식은 컴퓨터가 I/O 하드디스크에 접근할 때, 혹은 특정 함수를 불러왔을 때를 떠올리면 된다. 프로세스가 특정 I/O로 자원에 접근하면 첫 작업(Operation)이 끝날 때까지 다음 작업을 수행할 수 없는 것이다. C/C++, 자바와 같은 자바스크립트보다 상대적으로 오래된 언어들은 이 방식을 사용하는데, 이러한 방식은 기존에 네트워크 혹은 클라우드 환경에서의 프로그래밍이 고려되지 않았을 때의 방식이었다.

많은 컴퓨터 아키텍트들이 고심해 만든 기존의 방식은 우리 생활에 많은 변화를 가져오긴 했지만 시나리오가 ‘컴퓨터 1 : 사용자 1’에 한정돼 있었기 때문에 네트워킹과 인터넷, 게다가 클라우드 환경이 고려돼야 하는 현대적인 프로그래밍에서는 한계에 부딪혔고, 이런 근본적인 제약 사항은 멀티스레딩과 같은 기술로 커버해왔다.

멀티스레드 방식은 프로세스를 이루는 스레드를 여러 개로 늘려 공통으로 사용하는 메모리 공간을 공유하면서 메모리가 쓰이지 않을 때마다 스레드가 공유 메모리를 취득하는 형식을 취한다. 이 방식은 프로그래머에게 혼란을 주고 제어 불가능한 상황을 초래할 수 있다는 것이 단점이다. 전에 겪어보지 못한 이상한(이른바 ‘듣보잡’) 버그가 발생하기도 하고, 사람의 두뇌에도 한계가 있는 것처럼 스레드가 많아질수록 그만큼 언제 발생할지 모르는 다음 단계에 대한 수행 처리가 미흡해질 수도 있다. 이런 문제들을 개선한 방식이 이벤트 기반 프로그래밍이다. 싱글스레드에 이벤트 프로그래밍 스타일을 써본 개발자라면 알겠지만, 이 방식은 굉장히 세련된 프로그래밍 스타일이고 멀티스레드 프로그래밍을 할 때처럼 제어하기 어렵지도 않다. 또한 콜백 함수를 사용해 특정 이벤트가 발생했을 때의 로직을 구현하므로 보다 평이하게 설계·구현할 수 있다는 것이 장점이다.

Node.js의 모든 I/O 또한 비동기로 실행된다. 이벤트 루프를 기반으로 비동기로 실행되므로 앞서 설명한 바와 같이 I/O의 결과를 직접 돌려받는 대신 콜백 함수의 파라미터로 전달받게 된다. 물론 일부 API는 동기 형식으로도 사용할 수도 있다.

<리스트 1> request started 메시지 수행var server = require('http');
server.createServer();
server.on('request',function(req, res) {
console.log('received request');
});server.listen(9000, 'localhost');

<리스트 1>은 이벤트를 설정하고 값이 오면 ‘request started’라는 메시지를 수행하는 예제다. Server.on(‘request’)에서 request 이벤트에 콜백 함수를 등록하고 중괄호 안의 로직(여기서는 메시지 로그 출력)을 실행한다. Request가 몇 개든지, 언제오든지, 발생할 때마다 로직을 수행하게 된다.

<리스트 1>의 코드는 Node.js와 관련해서 쉽게 찾아볼 수 있는 예제다. 이번 연재에서 살펴보려는 주제인 ‘Node.js 내부 계층에서의 데이터 연동’의 관점에서 이 코드가 어떤 방식으로 동작되는지를 알아보자.

tech_img1211.jpg

<그림 2>는 크게 세 가지 컴포넌트로 나눠져 있다. 각 컴포넌트에 대해서 알아보면 다음과 같다.

- 개발자/사용자 그룹 : 여기서는 완성된 Node.js 애플리케이션을 사용하는 사용자와 Node.js 모듈을 사용해 프로그래밍하는 개발자 모두를 포함한다.
- Node.js 자바스크립트 모듈 : Node.js 프로그래밍 시에 주로 사용하게 되는 Node.js 기본 모듈 및 NPM을 나타낸다. 여기서는 그 모듈 중 파일시스템과 네트워크 관련 모듈만을 표현냈다. 왜냐하면 이 두 관련 모듈이 비동기적인 프로그래밍과 관련돼 있기 때문이다.
- V8 확장 계층 : 가장 큰 계층으로 Node.js의 핵심 계층을 나타낸다. 자바스크립트로 실행된 커맨드와 함수들을 분류하고 비동기적으로 수행되도록 하는 로직을 포함하고 있다. 사실 자바스크립트의 비동기 API를 사용한다 해도 OS는 기본적으로 멀티스레드 방식으로 설계돼 있기 때문에 이를 비동기적으로 수행되도록 제어하는 계층이 필요하다. 여기서는 libuv라는 라이브러리에서 그 역할을 담당한다. libuv가 핵심 계층이나 다름이 없는데, 여기서 I/O에 밀접한 시나리오가 발생하면 Node Binding 계층에서는 이를 libuv에 위임한다. libuv에서는 우리가 알고 있는 이벤트 루프를 만들고 이러한 I/O 동작을 분배하게 된다.

앞서 명시한 몇 가지 계층 이외에는 모든 작업이 멀티스레드로 동작된다는 점을 눈여겨볼 필요가 있다. 특정 계층에서만 I/O에 대해 비동기적인 제어가 실행되기 때문에 Node.js 또한 비동기적인 성격을 갖게 된 것이다. 그럼 <그림 2>에 명시된 플로우에 대해서 알아보도록 하자.

1) 개발자/사용자 그룹에서 파일시스템 혹은 네트워크 관련 비동기적인 함수를 실행한다.
2) ①번의 자바스크립트 블록은 곧바로 V8 확장 계층으로 넘어간다.
3) ②, ③번의 모든 요청을 우선 큐에 할당한다. 여기서는 모든 요청을 받지만 I/O적인 시나리오는 따로 큐에 쌓아두고 Process Tick Info와 같은 객체로 관리하게 된다. 일단 분류되면 모든 시나리오는 libuv로 위임되며, 작업 완료 시 콜백 함수가 실행된다. 이 콜백 함수 또한 큐로 관리된다.
이와 같은 간략한 순서로 모든 비동기적인 함수가 실행된다.



간략하게 살펴보는 V8 엔진의 특징

더욱 자세한 V8 엔진의 구조에 대한 설명은 다음 연재에서 다룰 예정이지만, 더욱 효율적인 설명을 위해 간단하게나마 V8 엔진의 특징을 살펴보고 넘어가자. V8 엔진은 구글 크롬 브라우저에서 자바스크립트를 네이티브 기계 언어로 컴파일하는 역할을 한다. 본래 브라우저에서는 자바스크립트 엔진이 있기 마련인데 V8 엔진은 구글 크롬 브라우저에서 더욱 동적이고 최적화된 성능을 위해 구현한 자바스크립트 엔진이다. 구글 V8 엔진에는 다음과 같이 알아두면 좋은 몇 가지 기본 용어가 존재한다.

- isolate : 가상화 머신(Virtual Machine) 객체를 뜻하며 생성되면 자기 자신만의 힙(Heap)을 가지고 동작하는 특성이 있다.
- Handle : V8 엔진의 모든 객체들은 이 핸들을 통해서 접근 및 활용되는데, 핸들은 단순히 특정 객체에 대한 포인터라고 보면 된다.
- scope : 범위(scope)란 핸들을 여러 개 가지고 있는 컨테이너라고 볼 수 있다. 다른 OS 혹은 엔진과는 달리 객체 포인터에 대한 사용이 끝나면 핸들이 속해 있는 scope를 삭제하면 된다.
- context : 자바스크립트 언어에서 컨텍스트라는 용어를 들어본 적이 있을 것이다. 컨텍스트라는 용어는 자바스크립트와 같은 동적언어에서 빈번히 쓰이는 개념이며, 클로저(Closure)와도 연관돼 있다. 즉 자바스크립트 코드가 실제적으로 영향을 미칠 수 있는 범위이며, 자바스크립트 엔진에서는 반드시 이러한 범위를 명시하고 있어야 정확한 작업이 진행될 수 있다.

지금까지 설명한 내용을 예제를 통해 살펴보자.

<리스트 2> Hello World 예제#include
using namespace v8;
int main(intargc, char* argv[]) {
Isolate* isolate = Isolate::GetCurrent(); - 1)
HandleScopehandle_scope(isolate); - 2)
Handle context = Context::New(isolate); - 3)
Context::Scope context_scope(context); - 4)
Handle source = String::NewFromUtf8(isolate, "'Hello' + ', World!'");
Handle< Script> script = Script::Compile(source); - 5)
Handle result = script->Run();
String::Utf8Value utf8(result);
printf("%s\n", *utf8);
return 0;
}

1) 기본 isolate를 GetCurrent라는 메소드를 통해 생성한다.
2) 핸들의 범위를 지정한다. 여기서 스택이 할당된다.
3) 자바스크립트의 컴파일을 위한 컨텍스트를 생성한다.
4) Hello world를 컴파일할 컨텍스트를 생성한다.
5) 바로 위 코드에서 작성한 Hello World를 컴파일할 코드이다. 스크립트 타입으로 명명돼 있으며 이 스크립트를 실행함으로써 이 문자열을 출력한다.
<리스트 2>를 보면 생각보다 단순한 방식으로 작업이 수행된다는 것을 확인할 수 있다. 이와 같은 단순한 방식으로 간단한 Hello World를 프린트할 수 있으나, 실제 Node.js를 구성하는 계층 코드를 보면 <리스트 2>의 코드보다 상당히 복잡하다. 그 이유는 <리스트 2>의 코드는 응용 패턴으로 여러 가지 시나리오에 적용돼 쓰이고 있기 때문이다. Node.js 아키텍처의 상위 계층이라 할 수 있는 Node Binding의 코드를 분석하는 것이 Node.js가 동작하는 방식을 이해하는 데 더욱 도움이 될 것이다. 물론 앞으로 분석하려 하는 Node Binding 또한 앞서 설명한 기본적인 자바스크립트 컴파일 과정을 매우 빈번히 사용하고 있다.



Node Binding의 이벤트 처리 방식

Node.js는 우리가 일반적으로 알고 있는 것처럼 이벤트, 논블록킹 방식으로 동작한다. 이벤트 방식으로 동작된다고는 하지만 실제로 IO에서 비동기적으로 동작하는지 확인하려면 디스크에 직접 접근해 활용되는 Node.js 모듈을 살펴봐야 한다. 일반적으로 알고 있는 fs 모듈이 이에 해당하므로, Node Binding 계층의 코드를 살펴보면서 분석해보도록 하자.

먼저 fs 모듈의 API들을 확인해보면 일종의 패턴이 존재하는 것을 알 수 있다. 이는 Async적인 메소드와 Sync적인 메소드를 전부 제공하고 있다는 것을 의미한다. 즉, <리스트 3>처럼 비동기/동기적인 함수로 나눠져 있는 것을 볼 수 있다.

<리스트 3> fs 모듈에 구분된 비동기/동기 함수fs.write (fd, buffer, offset, length, position, callback)
fs.writeSync(fd, buffer, offset, length, position)

비동기적인 메소드는 항상 마지막 파라미터에 콜백 함수를 지정한다. 이러한 메소드가 하위 레벨에 속하는 Node Binding에서는 어떤 식으로 변환될까 Node.js의 기본 모듈을 비롯해 NPM 또한 Node Binding을 거치게 돼 있다. 이는 Node.js의 기본 모듈이 Node Binding 내부의 node_xxxx.cc 클래스를 모두 참조하기 때문이다. NPM은 기본모듈을 응용해 작성한 것이므로 모두 이러한 형태의 클래스를 거치게 된다. 앞서 예를 든 fs 모듈은 node_file.cc 클래스를 통해서 API를 실행한다. 즉, 비동기/동기적인 함수를 모두 처리하는 로직을 지니고 있으며, fs 실행 처리 요청에 대한 로직은 <리스트 4>와 같은 클래스에서 실행된다.

<리스트 4> fs 실행 처리classFSEventWrap: public HandleWrap {
public:
static void Initialize(Handle< Object> target,
Handle< Value> unused,
Handle< Context> context);
static void New(constFunctionCallbackInfo< Value>&args);
static void Start(constFunctionCallbackInfo< Value>&args);
static void Close(constFunctionCallbackInfo< Value>&args);
private:
FSEventWrap(Environment* env, Handle< Object> object);
virtual ~FSEventWrap();static void OnEvent(uv_fs_event_t* handle, const char* filename, int events,
int status);uv_fs_event_t handle_;
bool initialized_;
};

<리스트 4>의 클래스인 FSEventWrap에서 많은 것을 확인할 수는 없지만 다음의 사항은 파악할 수 있다.

- FSEventWrap의 클래스 이름이 뜻하는 대로 파일시스템에 대한 요청과 이벤트를 처리하는 기본적인 메소드를 담고 있다.
- Handle 타입을 선언하고 있으며, 예제에서 본 Value와 Context 타입도 볼 수 있다. 또한 FSEventWrap 클래스는 HandleWrap 클래스를 상속받는다. HandleWrap 클래스는 Handle 클래스의 레퍼(wrapper) 클래스이다.

이제 FSEventWrap의 객체를 생성하는 부분을 살펴보자.

<리스트 5> FSEventWrap의 객체 생성voidFSEventWrap::New(constFunctionCallbackInfo< Value>&args) {
assert(args.IsConstructCall());
HandleScopehandle_scope(args.GetIsolate());
Environment* env = Environment::GetCurrent (args.GetIsolate()); - 1)
newFSEventWrap(env, args.This());
}

앞서 V8 엔진을 설명할 때 살펴봤던 isolate, scope와 같은 설정을 확인할 수 있다. ①번 코드에서는 GetCurrent로 isolate 값을 불러오는 것을 볼 수 있는데, 이는 Environment 객체에 속해있는 메소드로 구성돼 있다. 즉, Node.js는 isolate 관련 로직들을 좀더 세분화하기 위해 Environment라는 클래스로 구분하고 있는 것이다.

좀더 깊게 들어가 보면, Enviroment 클래스의 특성을 볼 수 있는데 <리스트 6>과 같은 클래스 멤버 변수를 확인할 수 있다.

<리스트 6> 클래스 멤버 변수 확인v8::Isolate* const isolate_;
IsolateData* constisolate_data_;
uv_check_timmediate_check_handle_;
uv_idle_timmediate_idle_handle_;
uv_prepare_tidle_prepare_handle_;
uv_check_tidle_check_handle_;
AsyncListenerasync_listener_count_;
DomainFlagdomain_flag_;
TickInfotick_info_;
uv_timer_tcares_timer_handle_;

<리스트 6>에서 uv_xxx로 시작하는 객체가 다수 보인다. 이는 이벤트를 관리하기 위한 객체로, 여기서는 주로 시간을 확인하고 있다. AsyncListner라는 클래스 또한 비동기적인 작업을 확인하려는 용도로 쓰인다. Enviroment 클래스에서는 isolate 객체들을 관리하기 위한 자료구조를 구성하고 있으며, isolate를 효율적으로 호출하기 위해 Wrapper 형식의 객체를 만들어 사용한다.

이렇게 작성된 Environment 클래스에서는 이벤트 루프를 두고 요청된 비동기적인 처리를 실행한다.

<리스트 7> 비동기적인 처리 실행inlineuv_loop_t* Environment::IsolateData::event_loop() const {
returnevent_loop_;
}
inlinebool Environment::AsyncListener::has_listener() const {
return fields_[kHasListener] > 0;
}

uv_loop_t의 타입을 활용해 Environment 클래스 상의 event_loop 메소드를 다루고 호출될 때마다 갱신된 값을 반환한다. 또한 비동기적으로 연동하는 객체가 있는지 확인해 값을 되돌려주고 있다. 그럼 궁극적으로 Node Binding 계층이 fs.write와 같은 요청을 받으면 어디로 전달될까 그 목적지는 node_file.cc 클래스의 다음 코드이다.

<리스트 8> Node Binding 계층에서의 fs.write 요청 전달#define ASYNC_DEST_CALL(func, callback, dest_path, ...) \
Environment* env = Environment::GetCurrent(args.GetIsolate()); \
FSReqWrap* req_wrap; \
char* dest_str = (dest_path); \
intdest_len = dest_str == NULL 0 :strlen(dest_str); \
char* storage = new char[sizeof(*req_wrap) + dest_len]; \
req_wrap = new(storage) FSReqWrap(env, #func); \
req_wrap->dest_len(dest_len); \
if (dest_str != NULL) { \
memcpy(const_cast(req_wrap->dest()), \
dest_str, \
dest_len + 1); \
} \
int err = uv_fs_ ## func(env->event_loop() , \
&req_wrap->req_, \
__VA_ARGS__, \
After); \
req_wrap->object()->Set(env->oncomplete_string(), callback); \
req_wrap->Dispatched(); \
if (err < 0) { \
uv_fs_t* req = &req_wrap->req_; \
req->result = err; \
req->path = NULL; \
After(req); \
} \
args.GetReturnValue().Set(req_wrap->persistent());

기본적으로 이 함수는 C 프로그래밍의 매크로로 작성돼 있어서 분 매크로가 Node.js의 V8 확장 라이브러리에서 상당히 빈번하게 쓰이는 것을 볼 수 있는데, 사실 객체지향적인 설계에서 이러한 함수형 매크로는 컴파일러가 실행하기 전에 이미 실행이 완료되는 성질을 갖고 있어 디버깅 시 코드의 추적이 불가능하다. 이런 단점에도 불구하고 Node.js에서는 매크로를 빈번하게 사용함으로써 불필요한 로직의 공개를 꺼리고 있다. 로직의 공개를 꺼리는 것 이외에도 특정 로직의 계산을 빠르게 실행하기 위해 매크로를 사용한다.

<리스트 8>에서 구현되는 일은 다음과 같다.

1. 요청된 특정 이벤트의 처리를 위해 Environment::GetCurrent 메소드로 새로운 V8 Scope를 설정하고 생성한다.
2. ASYNC_DEST_CALL의 파라미터는 파일시스템 모듈과 관련된 메소드이므로 파라미터의 파싱 작업들을 수행한다.
3. uv_fs_ ## func의 형태를 가지고 있는 함수들을 event_loop에 할당한 후 Set 메소드를 사용해 설정한다(진하게 표시된 부분).

이 순서로 진행된 로직은 After라는 메소드를 호출한다. After 메소드에서는 fs 모듈에서 수행하는 API 각각의 로직을 분배하는 역할을 한다. After 메소드는 각 API 요청 케이스별로 시나리오를 수행하는 로직으로 구성돼 있다. 여기에는 일부 로직만 명시했다.

<리스트 9> After 메소드switch (req->fs_type) {
// These all have no data to pass.
case UV_FS_CLOSE:
case UV_FS_RENAME:
case UV_FS_UNLINK:
case UV_FS_CHMOD:
case UV_FS_FCHMOD:
argc = 1;
break;
case UV_FS_OPEN:
argv[1] = Integer::New(req->result, node_isolate);
break;
/////////////////////
case UV_FS_WRITE:
argv[1] = Integer::New(req->result, node_isolate);
break;
}

<리스트 9>는 일반적인 파일 모듈과 관련된 명령어를 처리한다. fs 모듈의 메소드와 비교하면 더욱 이해가 쉽다. 예를 들어 fs 모듈의 메소드에서는 파일 닫기, 읽기, 권한 모드 바꾸기(CHMOD), 쓰기(WRITE) 등의 케이스를 처리한다.

<리스트 9>의 코드는 node_file.cc 파일의 After 메소드의 스위치 구문 중 별도의 내용만을 따로 추출해 명시한 것이다. UV_FS_WRITE나 UV_FS_OPEN 케이스를 보면 뭔지는 정확히 모르겠지만, 적어도 한 줄의 커맨드를 받아서 실행한다는 것은 알 수 있다. 이 두 가지 케이스 이외의 다른 커맨드에서는 아무것도 실행하지 않는다. 이처럼 로직의 차이가 나는 이유는 단순히 UV_FS_WRITE나 UV_FS_OPEN의 케이스를 제외한 다른 커맨드에서는 별다른 데이터 전송이 일어나지 않기 때문이다. 이는 추후에 더 자세하게 살펴볼 것이다.

UV_FS_WRITE나 UV_FS_OPEN 케이스에서는 파일시스템에서 실행되는 특정 요청(여기서는 파일 열기와 쓰기)을 전역변수처럼 쓰는 argv의 배열의 값으로 할당하며 node_isolate 객체 값도 할당한다. 이때 node_isolate 객체 값은 Node.js만의 V8 내부 가상화 머신 객체를 뜻하며, 생성되면 자기 자신만의 힙을 가지고 동작하게 된다.

Node.js 파일시스템 모듈은 자바스크립트 계층에서 호출된 fs 모듈의 메소드가 Node Binding 계층을 거쳐 V8의 기능을 호출하는 구조이다. 이를 좀더 보기 쉽게 순서대로 정리해 설명하면 다음과 같다.

1. node_file.cc 파일에서는 Node.js의 fs 모듈에서의 다양한 파일시스템 제어 메소드 요청을 전달받는다. 예를 들어 fs.open 함수가 호출되면 먼저 node_file.cc에 해당 요청이 들어오게 된다.
2. node_file.cc는 NODE_MODULE_CONTEXT_AWARE라는 글로벌 매크로를 통해 InitFs 함수를 실행한다. InitFs 함수는 파일시스템을 활용하기 위한 초기화 단계를 거치게 된다.
3. InitFs 함수에서 NODE_SET_METHOD 매크로가 실행된다. 이 매크로는 node.h 파일에 정의돼 있으며, 자바스크립트의 범위와 메모리 핸들 등을 할당하고 데이터 타입을 받아들여 최초로 적응시키는 작업을 수행한다. 이와 관련된 내용은 <리스트 10>을 통해 확인할 수 있다.

<리스트 10> InitFs 함수에서 실행된 NODE_SET_METHOD 매크로template
inline void NODE_SET_METHOD(constTypeName&recv,
const char* name,
v8::FunctionCallback callback) {
v8::Isolate* isolate = v8::Isolate::GetCurrent();
v8::HandleScopehandle_scope(isolate);
v8::Local t = v8::FunctionTemplate::New(callback);
v8::Localfn = t->GetFunction();
v8::Localfn_name = v8::String::NewFromUtf8(isolate, name);
fn->SetName(fn_name);
recv->Set(fn_name, fn);

4. 요청이 들어오면 자동적으로 Open 메소드가 실행된다. 이 메소드에서는 특정 예외 처리, 즉 해당 파일에 대한 디렉터리 명이 잘 전달됐는지를 검사한다. 예외 처리를 거친 후 fs.open 함수의 세 번째 파라미터에 요청 값이 있으면 ASYNC_CALL 매크로를 실행한다. 이 때 ASYNC_CALL은 다시 ASYNC_DEST_CALL을 호출한다.
5. ASYNC_DEST_CALL의 정의는 앞서 살펴본 바 있다. 추가로 설명을 덧붙이자면 현재의 VM에 대한 정보를 찾아 할당하고 FSReqWrap이라는 객체를 새로 생성한다는 것이다. 마지막으로 이 매크로에서는 After 함수를 활용하게 된다. 여기서 After 함수는 필요한 데이터를 전달하고 MakeCallback 함수를 호출한다. 이때의 콜백 요청은 이벤트 큐에 할당돼 처리된다.
6. 5번에서 요청된 fs 모듈 관련 요청은 V8이 아닌 UV 라이브러리에서 처리된다. fs.c와 uv.h 파일에서 수행되는 것이다. fs.c 파일 내부에는 C 언어를 활용해 윈도우 혹은 유닉스 파일시스템을 다룰 때의 로우 레벨 함수를 작성해 놓았는데, uv_fs_open 함수가 호출됨으써 특정 파일을 여는 요청과 로직이 실행된다.
이와 같이 다소 복잡하게 보일 수 있는 과정을 거쳐 실제 파일이 오픈된다. 여기서는 V8 엔진 이외에도 UV 라이브러리가 아주 중요한 역할을 한다는 것을 확인할 수 있었다. Node.js의 전체 계층 중 이벤트 풀에 해당하는 것이라고 할 수 있다.



정리하며

이번 시간에는 Node.js의 계층 구조 가운데 Node Binding이라는 계층에 대해서 살펴봤다. 아직 Node.js의 내부 구조가 어떻게 동작하는지 파악하기에는 역부족일 수도 있지만, 다음과 같은 몇 가지 사실은 짚고 넘어가자.

- 주요 계층은 모두 C/C++로 작성돼 있다.
- Node Binding 계층은 V8 엔진에 컴파일되기 전에 주요 기본 모듈에 대한 API를 구성하는 내용을 담고 있으며, V8 엔진의 확장이라고 볼 수 있다.
- Node.js에서는 디스크와 네트워크 액세스와 관련한 API들에서 비동기 방식을 고수하고 있다. 이 방식은 이벤트 루프로 동작하며 이벤트 루프의 알고리즘은 큐와 버퍼로 관리된다.

다음 시간에는 V8 엔진과 libuv와 같이 더욱 로우 레벨에 속하는 라이브러리에 대해서 알아본다.