벤권의 개발자 이야기

Node.js Streams에 대하여 1부 본문

-/node

Node.js Streams에 대하여 1부

벤권 2022. 2. 24. 00:33

Node.js Streams에 대하여

1부 시작

왜 Stream에 대하여 알아보게 되었는가? 

원하는 파일들을 하나의 zip파일에 옮겨넣어야 하는 개발을 하고있었다.

내가 원하는 이 기능이 adm-zip이라는 모듈에 그대로 구현되어 있어서 사용을 했는데 테스트 중에 큰 문제를 발견했다.

adm-zip은 기본적으로 파일을 전부 읽어 버퍼에 담은 후 zip파일에 옮겼다. 만약 파일 용량이 크면 결국 메모리 사용량이 매우 커지고 결국 서버에 엄청난 과부하가 걸리게 된다.

그래서 Stream형식으로 파일을 읽어와서 파이프로 zip파일에 옮겨야겠다고 생각했고 노드로 이 작업을 어떻게 해야할지 스택오버플로우에 질문을 해보기도 하고(답변은 달리지 않았다..) 구글링을 엄청한 끝에 archiver라는 아카이브를 생성하기 위한 스트리밍 인터페이스를 제공하는 모듈을 발견해서 해당 문제를 해결할 수 있었다.

archiver 모듈에 대해 한국어로 설명되어있는 블로그는 없는 것 같아서 이 모듈에 대해서도 나중에 다뤄볼까 한다.

우선 해당 모듈을 이용해 구현을 마치고 나서 노드에서 개발자들이 어려워하는 개념인 Stream에 대해서 공부하기로 결정하였다. Stream을 잘 공부해놓으면 나중에 내가 원하는 기능 구현을 위한 모듈이 없더라도 내가 직접 만들 날이 오지 않을 까 싶다. 실제 실무에서 multer같은 파일 업로드 모듈은 streams으로 대부분 구현되는데 모듈은 커스터마이징해서 쓰기 힘들어서(또한 multer는 express에 최적화 되어있어서) 파일 업로드 관련해서 직접 streams으로 구현한다고 한다. 

 

참고 문헌

https://www.freecodecamp.org/news/node-js-streams-everything-you-need-to-know-c9141306be93

 

Node.js Streams: Everything you need to know

> Update: This article is now part of my book “Node.js Beyond The Basics”. Read the updated version of this content and more about Node at jscomplete.com/node-beyond-basics [https://jscomplete.com/g/node-streams]. Node.js streams have a reputation for

www.freecodecamp.org

이번 포스팅은 freecodecamp에 올라와있는 해당 글을 보고 내가 나중에 다시 볼 수 있게 정리해나가려고 한다.

(사실상 번역이다. 최대한 자연스럽게 번역해보겠다! 따라서 본문과의 차이가 있을 수 있다.)

 

그럼 시작!!

 

스트림이란 무엇인가?

스트림이란 데이터의 collections이다. 마치 Strings나 arrays과 비슷하다고 볼 수 있다. Strings나 arrays과의 차이점은 스트림은 한번에 사용가능하지 않고 데이터 전체가 메모리에 한번에 들어와있을 필요가 없다.

이러한 특징때문에 스트림은 많은 양의 데이터를 처리할 때 매우 강력하다. (그래서 이전 포스팅에서 spawn은 스트림 인터페이스를 제공하기 때문에 큰 용량의 데이터를 처리할 때 유용하다는 점을 알 수 있다)

하지만 이런 큰 데이터를 처리할 때의 강력함 말고도 스트림은 유용한 점이 있다. 바로 조합성이다. 일반적으로 Linux Shell에서 pipe를 이용할 때 메인 명령어에서 다른 명령어로 데이터를 보내줄 수 있다. 이 처럼 노드에서도 스트림을 이용하여 똑같은 작업을 할 수 있다.

ls -al | grep my.png
const ls = ... // A Steram for the grep output
const grep =... // A stream for the wc input

ls.pipe(grep);

Screenshot captured from my Pluralsight course — Advanced Node.js

위 스트림 리스트는 노드의 Native 객체들의 예시들이다. 위 스트림에는 readable 스트림도 있고 writable스트림도 있고 TCP sockets이나 zlib, crypto과 같은 readable이면서 writable한 스트림도 있다.

 

위 오브젝트들은 매우 연관되어 있다. HTTP responses가 클라이언트에선 readable 스트림이지만 서버에선 writable 스트림이다.

 

또한 stdio 스트림 (stdin, stdout, stderr)같은 경우 메인 프로세스와 자식 프로세스가 서로 반대의 타입을 가지게 된다.

위에서 http response가 클라이언트, 서버의 입장에서 각각 readable, writable인것 처럼 말이다.

 

 

스트림의 노드 실제 예제

항상 이론은 중요하지만 결국 뇌에 조금이라도 각인을 시키려면 실제로 코드를 작성하고 100% 내가 배운대로 작동하는 지 확인을 해야한다.

 

다음 코드를 보자

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();

자 이런식으로 big.file이라는 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);

위 서버는 big.file을 비동기적으로 불러와 서빙하게 된다. 해당 코드를 실행하게 되면?...

바로 메모리 사용량을 보면 400MB가량이 증가하는걸 볼  수있다. 400MB니까 망정이지 2기가 3기가 100기가 점점 용량이 커질 수록 내가 경험했던 것처럼 심각한 과부하를 경험하게 된다.

 

그럼 이 문제를 스트림을 이용하여 해결해보자.

HTTP response 객체는 위에서 우리가 보았듯이 서버 입장에서 writable 스트림이다. 이는 big.file을 받는 readable stream이 있다면 우리는 이 둘을 파이프로 연결하여 메모리의 과부하없이 데이터를 전송할 수 있다.

 

우리가 흔히 알고있는 fs 모듈은 여러가지 stream을 제공하는데 createReadStream메서드를 이용해서 readbleStream을 만들 수 있다.

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);

위 코드를  통하여 우리는 메모리 사용량이 25MB가량으로 줄어든 것을 확인 할 수 있다. 파일 용량이 얼마나 커지던간에 메모리 사용량은 25MB가량으로 유지될 것이다. 그리고 노드에서 buffer limit의 기본값을 벗어는 파일도 전송할 수 있게 된다.

 

 

Comments