toggle menu

[NodeJS] Stream의 개념과 Stream2의 차이

2015. 6. 24. 14:02 Node.js

이 글은 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 에 대해서도 이해하게 되는 시간이 되었기를 기대한다.





Node.js 관련 포스팅 더보기