본문 바로가기

Back-end/Node.js

스트림 코딩 (Stream Coding) - (3) Writable

* 이 글은 Mario Casciaro, Luciano Mammino가 저서한 <Node.js 디자인 패턴 바이블> 서적을 참고한 게시글입니다.

 

서적 정보 : http://www.kyobobook.co.kr/product/detailViewKor.laf?mallGb=KOR&ejkGb=KOR&barcode=9788931464283

 

Node.js 디자인 패턴 바이블 - 교보문고

검증된 패턴과 기술을 이용한 수준 높은 Node.js | 이 책은 이미 Node.js를 처음 접한 후 이제 생산성, 디자인 품질 및 확장성 측면에서 최대한 활용하고자 하는 개발자를 대상으로 합니다. 이 책은

www.kyobobook.co.kr


 

 

이번엔 Writable 스트림에 대해 알아보자.

 

 

 

 

0.  write(), end()

 

Writable 스트림은 write()함수로 밀어넣고, end() 함수로 데이터의 끝을 알리는 간단한 메커니즘을 가지고 있다.

 

함수의 원형은 각각 이렇다.

 

// write()
writable.write(chunk, [encoding], [callback]);
// end()
writable.end([chunk], [encoding], [callback]);

 

optional 파라미터는 []로 표시해두었다.

 

 

우리가 자주 보는 http 서버에서도 이러한 기능을 제공하는데, request, response 객체에 대해 스트림 기능을 제공하고 있다.

 

 

import { createServer } from "http";
import Chance from "chance";
const chance = new Chance();
const server = createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
while (chance.bool({ likelihood: 95 }) {
res.write(`${chance.string()}\n`);
}
res.end("\n\n");
res.on("finish", () => console.log("All data sent"));
})
server.listen(3000, () => console.log("Listening On Port 3000"));

 

response를 보낼 때 스트림 형태로 작성을 할 수 있으며(코드에서는 랜덤 문자열을 생성해서 스트림에 밀고 있다), 마지막에 res.end()를 호출해 response를 보내줄 수 있다.

 

 

 

 

 

1.  Back Pressure

 

사실 Writable 스트림에서 가장 중요하게 느껴지는 부분이 이 backpressure 메커니즘이다.

 

지난 Readable 편에서도 잠깐 언급했었지만, 스트림에 쓰는 속도를 읽는 속도가 따라가지 못한다면 스트림 버퍼에 데이터가 쌓이게 되는 병목 현상이 일어날 수 있고, 이를 해결하는 메커니즘이 이 backpressure이다.

 

Writable 객체는 writableHighWaterMark라는 속성값을 가지고 있는데, 이는 스트림 버퍼 안에 보관할 수 있는 총 Byte를 의미한다. object 모드이면 object의 갯수를 나타낸다.

 

아무튼 이 수치에 도달할 때까지 consumer가 데이터를 소비하지 못한다면, 스트림은 새로운 데이터를 읽어오는 것을 잠시 멈춘다. 

 

물론 writableHighWaterMark는 일종의 threshold이지 limit이라는 의미까지는 아니다.

 

만약 강제로 write() 함수를 호출해버리면, false를 반환하긴 하지만, 버퍼에 계속 쓰는 것은 가능하다.

 

따라서 원한다면 버퍼에 더 많은 데이터를 쌓을 수는 있지만, 버퍼에 쌓이는 데이터의 양을 이 값 미만으로 유지하는 것이 Node.js의 가이드라인이니 따르는 것이 좋을 듯 하다.

 

writableHighWaterMark에 버퍼의 메모리가 도달하면, write() 함수는 false를 반환하고 버퍼에 여유가 생기면 "drain" 이벤트를 통해 다시 버퍼에 쓸 수 있음을 알려준다. 

 

이 메커니즘을 stream.once() 이벤트 리스너를 이용해서 매끄럽게 처리할 수 있는데, 위의 코드를 업그레이드 해보자.

 

 

import { createServer } from "http";
import Chance from "chance";
const chance = new Chance();
const server = createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain" });
function generateMore() {
while (chance.bool({ likelihood: 95 }) {
const randomChunk = chance.string();
}
const shouldContinue = res.write(`${chance.string()}\n`);
if (!shouldContinue) {
return res.once("drain", generateMore);
}
}
generateMore();
res.on("finish", () => console.log("All data sent"));
})
server.listen(3000, () => console.log("Listening On Port 3000"));

 

generateMore() 함수로 write()를 호출하는 메커니즘을 묶고, write()가 false를 뱉으면 "drain" 이벤트에 맞춰 다시 실행해준다.

 

 

다음은 Transform 스트림에 대해 알아보도록 하겠다.