이 글은 neethack.com 의 Understand Node Stream 을 번역한 글입니다.
Node.js는 이벤트 루프에 기반한 비동기 I/O 를 제공하고 있다. 파일 시스템에서 읽기/쓰기 자업을 할 때나 HTTP 요청을 전달할 때 Node.js는 노드는 응답을 기다리는 동안 다른 이벤트들을 처리할 수 있는데, 이를 non-blocking I/O 라고 부른다. Stream은 이보다는 더 확장된 개념으로 메모리 버퍼와 대역폭을 절약할 수 있는 이벤트 기반의 I/O 인터페이스를 제공한다.
filesystem 에서 읽기 작업 시, node는 callback과 함께 non-blocking method을 제공한다.
var require('fs'); fs.readFile('./test.json', function(data, err){ if (err) { return console.log(err); } console.log('test file is loaded:\n', data); });
그러나, 대용량 파일의 파일 같은 경우, 파일 전체를 모두 로드하기 전에 메모리 버퍼를 절약하기 위해 뭔가를 하고 싶어질 수도 있다. 이것이 stream이 등장하게 된 이유다.
var fs = require('fs'); var stream = fs.createReadStream('./test.mp4'); stream.on('data', function(data) { console.log('loaded part of the file'); }); stream.on('end', function () { console.log('all parts is loaded'); }); stream.on('error', function(err) { console.log('something is wrong :( '); });
기본적으로 읽기 스트림은 "data", "end", "error" 이벤트를 가진 EventEmitter 이다.
"data" 이벤트는 파일의 일부를 리턴한다.
"end" 이벤트는 읽기가 완료되었을 때 호출된다.
"error" 이벤트는 에러가 발생했을 때 호출된다.
그래서 우리는 파일이 전체로 로드될 때까지 기다릴 필요없이 파일을 일부를 쓰거나 어떤 처리를 할 수 있다. 인터넷에서 파일을 요청할 때를 예로 들어보자.
var fs = require('fs'); var request = require('request'); var stream = request('http://i.imgur.com/dmetFjf.jpg'); var writeStream = fs.createWriteStream('test.jpg') stream.on('data', function(data) { writeStream.write(data) }); stream.on('end', fucntion () { writeStream.end(); }); stream.on('error', function(err) { console.log('something is wrong :( '); writeStream.close(); });
위의 코드는 데이터의 일부를 받을 때마다 파일에 쓰게될 것이다.
pipe
Pipe 는입력을 출력으로 리다이렉트 할 수 있게 해주는 또다른 컨셉이다. 위의 파일 다운로드 예제는 아래와 같이 pipe로 표현할 수 있다.
var fs = require('fs'); var request = require('request'); var stream = request('http://i.imgur.com/dmetFjf.jpg'); var writeStream = fs.createWriteStream('./testimg.jpg'); stream.pipe(writeStream);
pipe가 하는 일은, pipe로 stream 간에 read 와 write event 들을 연결해주는 것이다. 때문에 우리는 심지어 여러개의 pipe를 서로 연결할 수도 있다.
var fs = require('fs'); var request = require('request'); var gzip = require('zlib').createGzip(); var stream = request('http://i.imgur.com/dmetFjf.jpg'); var writeStream = fs.createWriteStream('./testimg.jpg'); // write gzipped image file stream.pipe(gzip).pipe(writeStream);
Stream2 (Readable and Writable stream)
'data' 이벤트에 기반한 스트림이 갖는 한가지 문제는 stream 을 읽는 타이밍이나 한번에 얼마나 많은 데이터를 읽을 지를 제어할 수가 없다는 점이다. data 이벤트가 걸리면, 핸들러는 버퍼에 데이터를 쓰거나 디스크에 정상적으로 써야한다. 이런 상황은 매우 느리거나 제한된 쓰기 I/O를 가진 경우에 문제가 된다. 그래서 노드 v0.10 부터 새로운 스트림 인터페이스를 stream2 라는 이름으로 선보였다.
Readable Stream
Readable stream 은 이전 stream 인터페이스와 더불어 새로운 'readable' 이벤트가 추가되었는데, 이 이벤트를 통해서 읽는 타이밍이나 얼마나 한번에 많이 읽을지를 제어할 수 있게 해준다.
// node.js v0.10 이상 var fs = require('fs'); var stream = fs.createReadStream('./testimg.jpg'); var writeStream = fs.createWriteStream('./output.jpg'); stream.on('readable', function () { // stream 이 읽을 준비가 됨 var data = stream.read(); writeStream.write(data); }); stream.on('end', function () { writeStream.end(); });
그래서 readable 이벤트가 걸리면, stream.read() 로 데이터를 읽는 것을 제어할 수 있다. 만약 데이터를 읽을 수 없다면, readable 이벤트는 다시 이벤트 루프에 던져지고 나중에 다시 걸리게 될 것이다.
Readable stream은 물론 하위 호환성을 갖기 때문에 'data' 이벤트 역시 받을 수 있고 이 경우 Stream은 readable 이벤트를 사용하지 않는다.
Writable Stream
Writable stream 은 새로운 'drain' 이벤트가 추가되었는데, 이 이벤트는 buffer에 있는 모든 데이터가 쓰여졌을 때 걸리게 된다. 이를 통해서 buffer가 비워졌을 때 데이터를 쓸 수 있도록 타이밍을 제어할 수 있다.
// node.js v0.10 이상 var fs = require('fs'); var stream = fs.createReadStream('./input.mp4'); var writeStream = fs.createWriteStream('./output.mp4'); var writable = true; var doRead = function () { var data = stream.read(); //만약 wriable이 false 를 리턴한다면, buffer가 꽉 차있다는 뜻이다. writable = writeStream.write(data); } stream.on('readable', function () { if(writable) { doRead() } else { // stream buffur가 꽉 찼으니 drain 이벤트가 발생할 때까지 대기 writeStream.removeAllListeners('drain'); writeStream.once('drain', doRead) } }); stream.on('end', function () { writeStream.end(); });
마치며
단순히 완료되고 나면 callback을 주는것과 달리, 중간중간 결과를 이벤트 방식으로 던져주는 것.. stream은 이런 배경에서 등장했다. 대용량의 데이터를 전달해야하는 경우에 노드의 이벤트 루프 기반의 stream의 효용이 가장 크게 드러나게 될 것이란 예상을 하는 것은 어렵지 않을 것이다.
stream의 개념과 함께 stream2에 대해서도 다루었는데, 기존 stream이 가지고 있던 문제를 극복하기 위해 제어적인 부분이 추가된 stream2 에 대해서도 이해하게 되는 시간이 되었기를 기대한다.