본문 바로가기

: Back-end

[Node.js] Stream

반응형

원문) https://medium.com/edge-coders/node-js-streams-everything-you-need-to-know-c9141306be93

Node.js Streams: Everything you need to know

[JS] Stream 에 대해 당신이 알아야 할 모든 것

 

Table of Contents

1. What are streams?

우선 node.js stream에 관한 공식 설명부터 읽어보자.

A stream is an abstract interface for working with streaming data in Node.js.
stream은 node.js에 스트리밍 데이터를 처리하기위한 추상 인터페이스이다.

ref) https://nodejs.org/api/stream.html

이걸로 충분한 설명이 되면 좋겠지만, 설명처럼 너무 추상적인 설명이 아닌가 싶다.
조금 더 풀어서 얘기해보겠다.

streams은 배열이나 문자열같은 데이터의 집합체이다. 핵심은 streams 는
한번에 동작하는 것이 아니라는 것이 큰 차이점이다. 즉, streams 는 하나의 chunk 단위로 데이터를 전달하는 특징을 가지고 있다. 또한 streams 는 메모리에 맞게 사용할 필요도 없다. 이러한 차이점들은 방대한 양의 데이터를 다룰 때 강력한 강점이 된다.

그러나 streams 는 방대한 양의 데이터를 다룰 때만 사용 되는 것은 아니다. 이 것은 마치 리눅스의 커맨드들을 연결(pipe)하여 사용하는 것 처럼, 우리의 코드를 연결할 수 있는 가능성(composability) 을 제공해준다.

~/learn-node $ grep -R exports * | wc -l 6

example. Composability with Linux commands

위의 예시는 아래와 매핑된다.

const grep = ... // A stream for the grep output
const wc = ... // A stream for the wc input

grep.pipe(wc)

노드의 많은 내장 모듈은 스트림 인터페이스를 구현합니다.

Screenshot captured from my Pluralsight course — Advanced Node.js


Screenshot captured from my Pluralsight course — Advanced Node.js

위 목록은 native node.js readable, writable streams 이 될수 있는 객체입니다. 물론 읽기/쓰기가 모두 가능한 것들도 있습니다. (ex. TCP sokets, zlib, crypto streams)

이 객체들 간 밀접한 관계가 있다. 혹시 발견하신 분? HTTP 응답은 클라이언트의 readable stream이다. 그리고 이것은 서버의 writable stream이 된다는 것이다.
이것은 HTTP의 경우이기 때문에 우리는 기본적으로 (http.IncomingMessage)와 같은 오브젝트로 읽고, (http.ServerResponse)으로 쓴다.

역시 우리는 stdio streams (stdin, stdout, stderr) 이 어떻게 동작하는지 살펴볼 필요가 있다. 자식 프로세스들로 부터 반대 스트림을 어떻게 갖는지 살펴보자.
그리고 이것들은 쉽게 메인 프로세스인 stdio streams 와 연결할 수 있다.

2. streams practical example

이론은 여기까지하고, 아래 예시를 살펴보자.

const fs = require('fs');
const file = fs.createWriteStream('./big.file');

for(let i=0; i<= 1e6; i++) {
file.write('Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n');
}

file.end();

fs(file system)을 통해서 writeStream을 생성하고, 거대한 사이즈의 텍스트 문서를 집어넣는다. 이 파일은 약 400MB이다.

아래는 대조군으로 big.file만 제공하는 서버 예시이다.

const fs = require('fs');
const server = require('http').createServer();

server.on('request', (req, res) => {
fs.readFile('./big.file', (err, data) => {
    if (err) throw err;

    res.end(data);
});
});

server.listen(8000);

서버가 요청을 받으면, 비동기 방식인 fs.readFile을 사용하여 큰 파일을 처리한다. 그렇다고 해서 이 동작이 다른 동작을 방해하거나 막지 않는다. 문제가 될 것은 아무것도 없다는 말이죠!! 이것이 핵심포인트이다.

그럼, 서버를 실행하고 연결하여 메모리를 모니터링할 때 어떤 일이 일어나는지 알아보자.

제가 서버를 실행했을 때, 메모리 양은 8.7MB로 시작한다. 나쁘지 않는 수치로 시작한다.

이 서버로 연결했을 때 메모리가 소비되는 현황을 살펴보자.

와우! 정말 놀랍게도 메모리 사용량이 434.8 MB 로 뛰었다. 이건 어떻게 안될까?

시스템은 기본적으로 큰 파일(big.file)을 메모리에 넣기 전에 response object를 메모리에 채운다. 이 방식은 매우 비효율적이다.

HTTP response ojbect는 역시 writable stream으로 쓰여진다. 이 말은 우리가 큰 파일(big.file)을 readable stream으로 가질 수 있다는 말이다. 우리는 이 스트림을 연결하여 400MB의 메모리 소모없이도 거의 같은 결과를 얻어 낼 수 있다.

Node의 fs 모듈은 createReadStream 메서드를 사용하여 모든 파일에 대해 읽을 수 있는 스트림을 제공해준다. 다음과 같이 응답 객체에 연결할 수 있다:

const fs = require('fs');
const server = require('http').createServer();

server.on('request', (req, res) => {
const src = fs.createReadStream('./big.file');
src.pipe(res);
});

server.listen(8000);

이제 당신이 서버를 연결하려 할 때, 아래와 같은 마법같은 일이 벌어진다! (메모리 사용량에 집중해라.)

What's happening?

만약 client측에서 큰 파일을 요청했을 때, 우리는 한번에 한덩어로 분리하여 가져온다. 즉, 서버는 메모리에 전혀 버퍼를 쌓지 않는다. 메모리 사용량은 대략 25MB 에서 멈춘다.

만약 위 예시보다 더욱 극한의 상황을 가정해보자. 위의 큰 파일(big.file)의 길이가 5배 정도 더 긴 거대한 데이터라 가정해보자. 그럼 이 파일은 약 2기가 이상일 것이다. 이는 굉장한 부담일 것이며 노드의 기본버퍼 사이즈를 뛰어 넘는 크기이다.

만약 fs.readFile을 사용하여 해당 파일을 제공하려고 하면 기본적으로 할 수 없습니다(제한을 변경할 수 있음). 그러나, fs.createReadStream을 사용하면 요청자에게 2GB의 데이터를 스트리밍하는 데 전혀 문제가 없으며 무엇보다 프로세스 메모리 사용량이 대략 25MB 수준과 동일하게 낮게 측정될 것이다.

이제 조금 stream에 대해 배울 의욕이 생기는가?

Streams 101 (4 Stream Types)

node.js에는 4개의 스트림 타입이 존재한다. (Readable, Writable, Duplex, and Transform)

  • Readable: 은 데이터가 소비되어질 소스를 추상화 한 것이다. 예시. fs.createReadStream
  • Writable: 데이터가 쓰여질 수 있는 종착점을 추상화 한 것이다. 예시. fs.createWriteStream
  • Duplex: Readable과 Writable을 동시에 지원하는 것이다. 예시. TCP socket
  • Transform: 기본적으로 duplex stream이다. 데이터를 읽거나 기록할 때 수정/변환될 수 있는 데이터이다. 예시로는 gzip을 이용해 데이터를 압축하는 zlib.createGzip 스트림이 있다.

모든 streams는 EventEmitter의 인스턴스이다. 즉, 이벤트를 방출(emit)할 수 있다. 그러나 우리는 pipe를 통해서 조금 더 쉬운 방법으로 스트림 데이터를 사용할 수 있다.

 

이 후의 내용은 사용 사례에 대해서 따로 정리한 것.
만약 이어지는 내용이 궁금하다면 본문을 찾아가도록 하자.

ReadableStream

readable stream은 데이터가 소비되어질 소스를 추상화 한 것이다. example. fs.createReadStream

WritableStream

writable stream은 데이터가 쓰여질 수 있는 종착점을 추상화 한 것이다. example. fs.createWriteStream

Stream api example with multer

데이터를 받기 위해 multer 를 사용하였다.

import * as multer from 'multer';
import { NextFunction, Request, Response, Router } from 'express';

const router = Router();
router.post('/items',
             multer({ storage: multer.memoryStorage() }).any(),
             wrap(this.post.bind(this)));
async post(req: Request, res: Response, next: NextFunction): Promise<Response | void> {
    if(req.headers['content-type'].includes('multipart/form-data')) {
        if (Array.isArray(req.files) && req.files.length === 1 && req.files[0].fieldname === 'primary_item') {
        const files: Express.Multer.File[] = req.files;
        const myFile = files[0];
        const stream = Readable.from(myFile.buffer);
        }
    }
}

sample code (not tested)

 

multer({ storage: multer.memoryStorage() }).any()를 사용한 이유는 입력받은 data의 key값이 다르게 받을 수 있도록 하기 위함이다.
아니라면 .single() 사용하면 된다.

참고 문서)

https://nodejs.org/docs/latest-v14.x/api/stream.html

거의 같은 내용인데 더 잘 정리하신 분이 계십니다.
https://jeonghwan-kim.github.io/node/2017/07/03/node-stream-you-need-to-know.html

반응형

': Back-end' 카테고리의 다른 글

RabbitMQ vs Apache Kafka 비교  (2) 2023.03.06
2 Phase Commit  (0) 2022.06.17
공부하면서 정리가 필요한 목록  (0) 2022.01.18