원문 sitepoint.com의 Introduction to Node.js Streams(http://sitepoint.com/introduction-to-streams/)
오역 주의 - 번역도 하다보면 나아질런지... :-)
확장성. 빅데이터. 실시간... 이것들은 최근 인터넷이 직면한 몇가지 도전 과제들이다. 또한 이것은 Node.js와 Node.js의 넌블럭킹(non-blocking) I/O 모델이 나오게 된 배경이라고도 할 수 있다. 이 글에서는 Node의 데이터 집약적인 컴퓨팅을 위한 가장 강력한 API들 중 하나인 스트림(streams)에 대해서 소개할 것이다.
다음 예제를 생각해 보자:
var http = require('http')
, fs = require('fs')
;
var server = http.createServer(function (req, res) {
fs.readFile(__dirname + '/data.txt', function (err, data) {
res.end(data);
});
});
server.listen(8000);
이 코드는 완벽하게 실행된다. 한가지 사실을 제외한다면 전혀 잘못된 점이 없다. 그것은 클라이언트에게 데이터를 보내기 전 data.txt 파일 내용 전체를 버퍼링한다는 것이다. 애플리케이션에 클라이언트의 요청이 증가하면 많은 메모리를 사용하게 되고 또한 지연시간이 증가하게 되어 클라이언트는 서버 애플리케이션에서 전체 파일을 읽을때까지 기다려야만 한다.
다른 예제를 보도록 하자:
var http = require('http')
, fs = require('fs')
;
var server = http.createServer(function (req, res) {
var stream = fs.createReadStream(__dirname + '/data.txt');
stream.pipe(res);
});
server.listen(8000);
여기에서는 확장성에 대한 이슈를 극복하기 위해 스트림 API를 사용한다. 스트림 객체를 사용함으로써 data.txt 파일을 클라이언트에게 보낼때 서버 버퍼링이나 클라이언트의 대기시간 없이 디스크에서 읽어서 바로 한번에 하나의 청크(chunk, 데이터 단위)를 전송하는 것을 보장한다.
스트림이란 데이터의 입, 출력 시 비동기적으로 처리될 수 있는 데이터의 연속된 흐름이라고 정의할 수 있다. Node.js에서는 스트림을 읽을(readable) 수도 있고 쓸(writable) 수도 있다. readable stream(읽기용 스트림)은 데이터 청크가 도착할 때마다 data
이벤트를 발생시키는 EventEmitter 객체이다. 바로 이전 예제에서는 HTTP 클라이언트에게 파일의 내용을 pipe 하기 위해 readable stream이 사용됐다. 스트림이 파일의 끝에 닿으면 더이상 data
이벤트가 발생하지 않는다는 것을 알려주는 end
이벤트를 발생시킨다. 또한 readable stream은 잠시 중지시키거나 재시작 시킬 수 있다.
반면 writable stream(쓰기용 스트림)은 데이터의 스트림을 받는다. 이것 역시 EventEmitter 객체를 상속하고 write() 와 end() 메소드 두가지를 구현하고 있다. 첫번째 메소드는 버퍼에 데이터를 쓰는 것으로 데이터가 정확하게 쓰여져 메모리가 비워지면(flush) true를 반환하고 만약 버퍼가 꽉 차게 되면 false를 반환하게 된다(이번 경우는 데이터가 늦게 전송될 것이다). end() 메소드는 단순히 스트림이 끝났다는 것을 나타낸다.
그럼 스트림에 대해서 자세히 살펴보자. 그러기 위해서 우리는 간단한 파일 업로드 프로그램을 만들어 볼 것이다. 우선 readable stream을 사용하여 파일을 읽고 특정 목적지에 데이터를 pipe 시킬 클라이언트를 만들어야 한다. pipe의 반대편 끝에는 writable stream을 사용하여 업로드된 데이터를 저장하는 서버를 구현할 것이다.
클라이언트부터 시작해 보자. 우선 HTTP 및 file 시스템 모듈을 임포트하여 시작한다.
var http = require('http')
, fs = require('fs');
그리고 HTTP 요청(request)을 정의한다.
var options = {
host: 'localhost'
, port: 8000
, path: '/'
, method: 'POST'
};
var req = http.request(options, function(res) {
console.log(res.statusCode);
});
이제 파일을 읽을 readable stream을 생성하고 request 객체에 파일 내용을 pipe 한다.
var readStream = fs.ReadStream(__dirname + "/in.txt");
readStream.pipe(req);
모든 데이터를 읽어서 스트림이 끝나면 서버와의 연결을 끊고 request의 end() 메소드를 호출한다.
readStream.on('close', function () {
req.end();
console.log("I finished.");
});
클라이언트에서 했던 것처럼 Node.js 모듈을 임포트하는 것으로 시작한다. 그리고, 텍스트 파일에 데이터를 저장할 새로운 writable stream을 생성한다.
var http = require('http')
, fs = require('fs');
var writeStream = fs.createWriteStream(__dirname + "/out.txt");
클라이언트에서 파일을 업로드할 수 있도록 새로운 웹서버 객체를 생성한다. request 객체로부터 데이터를 받으면 서버는 스트림을 호출하여 버퍼의 내용을 출력 파일로 쓰게 된다.
var server = http.createServer(function (req, res) {
req.on('data', function (data) {
writeStream.write(data);
});
req.on('end', function() {
writeStream.end();
res.statusCode = 200;
res.end("OK");
});
});
server.listen(8000);
createServer() 에서 반환되는 req 객체와 res 객체가 각각 readable stream과 writable stream이라는 것을 주목하기 바란다. 우리는 data
이벤트를 리스닝할 수 있고 모든 처리가 끝나면 클라이언트에게 결과를 파이프할 수도 있다.
이 글에서는 Node.js의 가장 강력한 부분 중 하나인 스트림 API에 대해서 소개를 했다. 다음주에는 Node.js에 내장된 다른 모든 유형들과 써드파티 스트림까지 스트림의 세계에 더 깊이 살펴볼 것이다.
헉~! 다음주라니... 연재인줄 알았으면 번역하지 말것을... :-)
Node.js 스트림 관련 문서
- Node.js Q&A [cookbook] Stream #1
- Node.js Q&A [cookbook] Stream #2
- Node.js Q&A [cookbook] Stream #3
- Node.js Q&A [cookbook] Stream #4
- Node.js Q&A [cookbook] Node.js의 본질 Stream 슬라이드 #1 (Node.js Korea Conference)
- Node.js Q&A [cookbook] Node.js의 본질 Stream 슬라이드 #2 (Node.js Korea Conference)
- Node.js Q&A [cookbook] child_process 모듈의 spawn 메소드는 stream과 밀접한 관련이 있다.
- Node.js Q&A [cookbook] 파일시스템(fs)의 스트림 기본구성
- STREAMS - Issac Z. Schlueter (Node.js Korea Conference)