Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

process.nextTick() 이해하기

이 포스팅은 Kishore Nallan가 How to Node에 올린 Understanding process.nextTick()을 Kishore의 허락하에 번역한 글입니다.(요즘은 왠지 번역위주가 되어버린듯 ㅡㅡ;;) process.nextTick은 Node에서 코드를 비동기로 실행하는 중요한 메카니즘을 제공하기 때문에 유용하다고 생각되어서 번역해서 올립니다.




몇몇 사람들이 process.nextTick()을 혼동하는 것을 보았습니다. process.nextTick()이 무엇을 하고 언제 사용하는지 살펴보겠습니다.

이미 알고 있겠지만 모든 노드 어플리케이션은 싱글스레드로 동작합니다. 이 말은 I/O와는 항상 분리되어 있고 노드의 이벤트 루프는 오직 하나의 작업과 이벤트만 처리한다는 의미입니다. 이벤트루프의 모든 Tick에서 Node가 처리하는 콜백을 큐에 추가하는 이벤트루프를 생각할 수 있습니다. 그래서 멀티코어상에서 노드를 운영하더라도 실제 처리에서 병렬구조(parallelism)의 어떤 이점도 얻을 수 없습니다. 모든 이벤트는 한번에 하나씩 처리된다. 그래서 노드는 I/O가 많은 작업에 적합하고 CPU 작업량이 많은 작업에는 적합하지 않습니다. 모든 I/O기반 작업에서 이벤트큐에 추가될 콜백을 쉽게 정의할 수 있습니다. 콜백은 I/O작업이 완료되면 실행되고 동시에 어플리케이션은 다른 I/O작업에 대한 요청을 계속해서 처리할 수 있습니다.

process.nextTick()은 액션의 실행을 이벤트루프의 다음 차례까지 실제로 연기합니다. 간단한 예제를 보겠습니다. 다음 Tick에서 호출해야 하는 foo()함수가 있다면 다음과 같이 사용합니다.


function foo() {
  console.log('foo');
}

process.nextTick(foo);
console.log('bar');

위의 코드를 실행하면 bar가 foo보다 먼저 출력됩니다. 이는 foo()의 호출을 이벤트루프의 다음 Tick까지 연기했기 때문입니다.


bar
foo

이는 setTimeout()을 사용해도 같은 결과를 얻을 수 있습니다.


setTimeout(foo, 0);
console.log('bar');


하지만 process.nextTick()는 단순히 setTimeout(fn, 0)의 별칭이 아니고 더 효율적입니다.

어디서 process.nextTick()를 사용할 수 있는지 살펴보겠습니다.




CPU 부하가 심한 작업의 실행을 다른 이벤트로 나누어 처리하기
CPU 작업량이 많은 계산을 계속해서 실행해야 하는 compute() 작업을 가정해 보겠습니다. 같은 노드 프로세스에서 HTTP요청의 처리같은 다른 이벤트도 처리해야 한다면 process.nextTick()을 사용해서 다른 이벤트를 처리하면서 compute()의 실행을 나누어서 처리할 수 있습니다.


var http = require('http');

function compute() {
  // 복잡한 계산을 계속해서 수행한다
  // ...
  process.nextTick(compute);
}

http.createServer(function(req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello World');
}).listen(5000, '127.0.0.1');

compute();


이 예제에서 compute()를 재귀적으로 호출하는 대신 process.nextTick()를 사용해서 이벤트루프의 다음 tick까지 compute()의 실행을 지연시켰습니다. 이렇게 함으로써 다른 HTTP요청이 이벤트루프에 큐에 들어온다면 다음 compute()가 호출되기 전에 처리될 것입니다. process.nextTick()를 사용하지 않고 그냥 compute()를 재귀적으로 호출했다면 프로그램은 들어오는 HTTP 요청을 처리할 수 없을 것입니다. 이부분은 직접 테스트해보세요.

안타깝게도 process.nextTick()를 사용해도 멀티코어 병렬구조의 이득을 전혀 얻지 못합니다. 하지만 process.nextTick()을 사용하면 어플리케이션의 다른 부분에서 CPU를 공유해서 사용할 수 있습니다.




콜백을 비동기로 유지하기
콜백을 받는 함수를 작성했을 때 이 콜백이 비동기로 실행됨을 항상 보장해야 합니다. 이 규칙을 위반하는 예제를 살펴보겠습니다.


function asyncFake(data, callback) {        
  if(data === 'foo') callback(true);
  else callback(false);
}

asyncFake('bar', function(result) {
  // 이 콜백은 실제로는 동기로 실행된다!
});


왜 이 방법이 잘못되었는지 Node 레퍼런스 문서에서 가져온 예제를 보겠습니다.


var client = net.connect(8124, function() { 
  console.log('client connected');
  client.write('world!\r\n');
});


위 예제에서 어떤 이유에서든 net.connect()가 동기로 실행된다면 콜백은 즉시 실행됩니다. 그러므로 client 변수는 client에 내용을 작성하려고 콜백에서 접근했을 때 초기화되지 않았을 것입니다.

asyncFake()를 다음과 같이 항상 비동기로 실행되도록 고칠수 있습니다.


function asyncReal(data, callback) {
  process.nextTick(function() {
    callback(data === 'foo');       
  });
}





이벤트가 발생했을 때
소스를 읽어서 읽은 내용의 청크(chunk)를 담고 있는 이벤트를 발생시키는 라이브러리를 작성해 보겠습니다. 이 라이브러리는 다음과 같을 것입니다.


var EventEmitter = require('events').EventEmitter;

function StreamLibrary(resourceName) { 
  this.emit('start');

  // 파일을 읽고 읽은 청크마다 다음을 수행한다.       
  this.emit('data', chunkRead);       
}
StreamLibrary.prototype.__proto__ = EventEmitter.prototype;   // EventEmitter를 상속받는다



다른 곳에서 이 이벤트를 리스닝하고 있다고 하겠습니다.


var stream = new StreamLibrary('fooResource');

stream.on('start', function() {
  console.log('Reading has started');
});

stream.on('data', function(chunk) {
  console.log('Received: ' + chunk);
});



위 예제에서 리스너는 생성사가 호출되면서 즉시 발생하는 StreamLibrary의 이벤트인 start 이벤트를 절대 얻지 못할 것입니다. 이벤트가 발생할 때는 start 이벤트에 아직 콜백이 할당되지 않았습니다. 그래서 이 이벤트를 받지 못합니다. 여기서도 process.nextTick()를 사용해서 리스너가 start 이벤트를 찾을 수 있도록 이벤트의 발생을 지연시킬 수 있습니다.


function StreamLibrary(resourceName) {      
  var self = this;

  process.nextTick(function() {
    self.emit('start');
  });

  // 파일을 읽고 읽은 청크마다 다음을 수행한다.        
  this.emit('data', chunkRead);       
}



process.nextTick()에 대한 궁금증이 풀렸기를 바랍니다. 빠뜨린 부분이 있다면 댓글로 공유해 주길 바랍니다.
2012/02/06 02:02 2012/02/06 02:02