안녕하세요. 볼드나인 백엔드 개발자 장유빈입니다.
개발을 하다 보면 데이터를 엑셀로 만들어서 사용자에게 제공해 줘야 하는 경우가 있는데요
저희 서버에서 구현된 방식은 제공하고자 하는 데이터의 크기가 얼마든 간에
전체 데이터를 메모리에 불러온 뒤 엑셀파일로 만들어서 전달해 주는 방식을 사용하고 있었습니다.
이를 메모리를 좀 더 적게 사용하면서 안정적으로 데이터를 제공하고자 stream 기능을 활용하여 개선을 하였는데요.
이 개선을 어떻게 진행했는지 공유하고자 하며
이번 글에서는 우선적으로 stream이 무엇인지에 대해서 짚어가도록 하겠습니다.
stream이란 무엇인가?
노드에서 streaming data를 다루기 위한 추상 인터페이스이며 많은 내장 모듈에서 구현이 되어 있습니다.
대표적으로 HTTP responses/requests, fileSystem 등이 있습니다.
stream은 원본 데이터를 청크, 스트링, 객체 단위로 쪼개서 읽은 후 목적지로 쓸 수 있도록 해주는 기능입니다.
이러한 기능적 특징으로 인해 큰 파일 혹은 많은 데이터를 넘겨줘야 하는 상황에서
원본을 한 번에 메모리에 올리지 않고 단위 크기로 가져와서 넘겨줄 수 있기 때문에 메모리 사용량을 줄일 수 있습니다.
stream은 이러한 기능을 구현하기 위해서 Readable, Writeable, Duplex, Transform, Pipe로 구성이 되며
Readable은 데이터를 읽어오고,
Writeable은 목적지에 데이터를 쓰고
Duplex는 Readable과 Writeable을 함께 제공하며,
Transform은 Duplex의 기능에 추가적으로 데이터를 읽고 쓰는 과정 중간에 변환할 수 있도록 해줍니다.
메모리 사용량 확인 예제
실제로 stream을 사용했을 때 효과가 있는지 확인해 보기 위해 대용량의 zip 파일(1.31GB)을 복사하는 코드를 작성해 보겠습니다.
node에서 파일을 다루는 모듈인 fs는 stream을 제공하며 제공하지 않고도 파일을 다룰 수 있기에 이를 통해 메모리 사용량 비교가 가능합니다.
우선 stream을 사용하지 않고 파일을 복사하는 코드로 테스트하겠습니다.
코드
import * as fs from "fs";
import * as ReadLine from "readline";
const rl = ReadLine.createInterface({
input: process.stdin,
output: process.stdout,
});
// 메모리에 데이터가 올라오는 것을 확실히 볼 수 있도록 이곳에 두었습니다.
const source = fs.readFileSync("./data/sing.zip");
rl.on("line", function (line) {
if (line === "copy") {
fs.writeFileSync("./data/copySing.zip", source);
console.log("Copy done");
} else if (line === "close") rl.close();
}).on("close", function () {
console.log("bye");
process.exit();
});
JavaScript
복사
위 코드를 실행하면 원본 파일의 데이터를 전체를 메모리에 올려놓기 때문에 메모리 사용량은 1.24GB인 것을 확인할 수 있습니다.(환경에 따라 다를 수 있습니다.)
stream을 사용해서 파일을 복사해 보도록 하겠습니다.
코드
import * as fs from "fs";
import * as ReadLine from "readline";
const rl = ReadLine.createInterface({
input: process.stdin,
output: process.stdout,
});
// 메모리에 데이터가 올라오는 것을 확실히 볼 수 있도록 이곳에 두었습니다.
const sourceStream = fs.createReadStream("./data/sing.zip");
rl.on("line", function (line) {
if (line === "copy") {
const desStream = fs.WriteStream("./data/copySing.zip");
sourceStream.pipe(desStream);
console.log("Copy done");
} else if (line === "close") rl.close();
}).on("close", function () {
console.log("bye");
process.exit();
});
JavaScript
복사
위 코드를 실행하면 메모리를 가장 많이 사용할 때 38.7MB인 것을 확인할 수 있습니다.(환경에 따라 다를 수 있습니다.)
정리
위 두 코드를 통해 확실히 stream을 사용하면 전체 데이터를 메모리에 올리지 않고도 데이터 이동을 할 수 있음을 알 수 있었습니다.
예제에서는 fs에서 구현된 stream을 사용했지만
다음 글에서는 writeable, readable을 직접 다루는 방법을 보여드리도록 하겠습니다.
감사합니다.