파일을 전송하는 방식에는 크게 2가지 방식이 있습니다. Buffer와 Stream으로 파일을 전송할 수 있습니다. 흔히 유튜브에서 영상을 볼 때 스피너가 계속 돌아가면 "버퍼링이 심하네"라고 합니다. 이 때 파일이 전송되는 속도가 느리다는 것을 의미합니다. 또한 OTT에서 영상을 송출할 때 스트리밍한다고 하는데, 이 때 스트림(Stream)이 파일을 전송하는 방식이 됩니다.
쉽게 이해하면 버퍼링은 영상이 재생될 수 있을 때 까지의 최소한의 데이터를 모으는 과정입니다. 에네르기파를 모은다고 할 수 있습니다.
스트리밍은 송출자가 수신자의 컴퓨터로 데이터를 조금씩 보내는 과정을 의미합니다. 스트림을 할 때 버퍼링이 사용됩니다.
Buffer Stream Pipe in Node.js
Buffer in Node.js
Node.js 에서는 Buffer를 다룰 수 있는 기본 모듈을 제공합니다.
먼저 Buffer를 생성하기 위해서는 from() 함수를 사용합니다.
const buffer = Buffer.from('버퍼로 변경할 데이터 변경할 데이터');
console.log('from() : ', buffer);
console.log('length : ', buffer.length);
console.log('type : ', typeof buffer )
console.log('toString' , buffer.toString());
buffer 변수를 여러가지 형태로 출력을 해보면 Buffer가 어떻게 작동하는지 이해할 수 있습니다. object 타입의 데이터로 변경되어 출력됩니다.
Buffer 객체는 concat() 함수를 사용해 여러개의 버퍼 객체를 합칠 수 도 있습니다.
const arr = [Buffer.from('first data'), Buffer.from('second data'), Buffer.from('third data')]
const buffer = Buffer.concat(arr);
console.log('buffer : ', buffer)
Buffer 객체는 alloc() 함수를 사용해 임의의 길이를 가진 빈 버퍼를 생성할 수 있습니다.
const buffer = Buffer.alloc(10);
console.log('buffer : ', buffer)
Stream Node.js
fs 모듈을 사용해서 Node.js 환경에서 파일을 읽고 쓸 수 있습니다. 이 때 사용하는 방식이 Buffer를 사용하는데요, 문제는 파일을 읽어올 때 만약 용량이 100GB의 데이터를 가져온다면 100GB를 담을 수 있는 버퍼를 위한 메모리가 존재해야 합니다. 메모리 사용이 굉장히 비효율적으로 작동하게 됩니다.
이 문제를 해결하기 위해 등장한 녀석이 바로 Stream 입니다. Stream은 버퍼를 작은 단위로 쪼개어 여러번 보내는 방식으로 작동합니다. 만약 데이터의 용량이 100GB라면 1MB식 나눠 수천번을 보내는 형식으로 메모리는 적게 사용하면서 데이터를 효율적으로 전송할 수 있습니다.
우선 file system 모듈을 가져와서 readStream 객체를 생성해줍니다. readStream은 원래 16KiB 단위로 가져오지만 fs.createReadStream에서 생성되는 stream은 64KiB를 기본단위로 가집니다.
const fs = require('fs');
const readStream = fs.createReadStream('./test.txt');
너무 큰 단위로 쪼개면 스트림이 어떻게 작동하는지 잘 안보이기 때문에 더 작은 단위 16Byte로 쪼개서 들고올 수 있습니다. 두번째 인자로 highWatermark에 숫자를 기입하시면 어떤 단위로 가져올지 지정할 수 있습니다.
const fs = require('fs');
const readStream = fs.createReadStream('./test.txt', {highWaterMark : 16});
readStream은 이벤트 리스너를 사용해 받아온 데이터를 핸들링 할 수 있습니다. highWaterMark를 16Byte로 지정했기 때문에 16Byte의 데이터가 들어올 때 마다 data 이벤트가 발생하고 핸들링을 할 수 있습니다. data 이벤트는 16Byte로 전달되는 데이터를 chunk 단위로 쪼개옵니다.
data 이벤트 뿐만 아니라 error 이벤트도 따로 핸들링 할 수 있습니다.
readStream.on('data', (chunk)=>{
data.push(chunk);
console.log('data : ', chunk.toString('utf8'), chunk.length);
})
readStream.on('end', ()=>{
console.log('end : ', Buffer.concat(data).toString('utf8'));
})
Stream Write Node.js
Stream으로는 읽는 것 뿐만 아니라 쓰기도 가능합니다.
writeStream 객체를 생성합니다.
Stream 객체에 데이터를 쓰기 위해서는 write() 함수를 사용합니다.
모든 쓰기가 종료되면 end() 함수를 호출합니다.
end() 함수가 호출되면 finish 이벤트를 발생시키고, 이벤트 핸들링을 해줄 수 있습니다.
const writeStream = fs.createWriteStream("./writeStream.txt");
writeStream.write("첫번째 문장입니다. \n");
writeStream.write("두번째 문장입니다.\n");
writeStream.write("세번째 문장입니다.\n");
writeStream.end();
writeStream.on("finish", () => {
console.log("모든 파일을 다 썼습니다.");
});
Stream Pipe Node.js
Stream Pipe는 ReadStream에서 데이터를 받아 WriteStream으로 바로 전달해주는 작업을 의미합니다. 스트림끼리 연결해주는 건데, 수도관을 연결하는 모양새라 pipe라고 부릅니다. 실제로 Stream을 사용할 때 pipe를 많이 사용합니다.
사용방법은 굉장히 간단합니다.
먼저 파일을 읽어 올 ReadStream 객체를 생성합니다.
데이터를 쓰기 위한 WriteStream 객체를 생성합니다.
연결이 시작되는 객체에서 pipe() 함수를 사용해 인자로 연결할 스트림 객체를 전달합니다.
// pipe
const readStream = fs.createReadStream('./test.txt');
const writeStream = fs.createWriteStream('./newTest.txt');
readStream.pipe(writeStream);
pipe는 하나만 연결할 수 있는게 아닙니다. Stream을 지원하는 메소드들을 계속해서 연결할 수 있습니다. 예를 들어 특정 파일의 데이터를 읽어와 압축한 후 WriteStream으로 데이터를 전달해 줄 수 있습니다.
const fs = require("fs");
const zlib = require('zlib')
// pipe
const readStream = fs.createReadStream('./test.txt');
const zipStream = zlib.createGzip();
const writeStream = fs.createWriteStream('./newTest.txt');
readStream.pipe(zipStream).pipe(writeStream);
Stream의 효용성
앞서 알아본 바로는 파일을 읽어올 때 파일 용량 전체를 메모리에 올린다고 했습니다. 즉 파일을 읽어오면 현재 사용중인 메모리 크기가 증가하는 것을 확인할 수 있습니다.
우선 용량이 큰 파일을 생성합니다. for문을 1천만번 반복하면서 WriteStream으로 파일을 생성했습니다. 이 파일의 용량은 600MB 가량 됩니다.
const fs = require('fs');
const file = fs.createWriteStream('./bigFile.txt');
for(let i=0; i<10000000; i++){
file.write(`${i} line : 용량이 큰 파일을 생성합니다. \n`);
}
file.end();
file.on('finish', ()=>{
console.log(`파일 쓰기가 완료되었습니다.`);
})
이렇게 생성된 파일을 읽어와보도록 하겠습니다. 먼저 시작전 memory 사용량을 찍어보고 이 후 파일을 읽어 메모리를 사용한 다음 memory 사용량을 찍어보겠습니다.
const fs = require("fs");
// Power of Stream
console.log('파일 올리기 전 : ', process.memoryUsage().rss);
const data1 = fs.readFileSync('./bigFile.txt');
console.log('파일 올린 후 : ', process.memoryUsage().rss);
파일을 읽어오니 32MB 였던 프로세스 메모리 용량이 순식간에 600MB를 넘어서버립니다.
큰 용량의 데이터가 메모리에 한번에 올라가는 일을 방지하기 위해 Stream을 사용합니다.
먼저 ReadStream으로 파일을 16KB 단위로 읽어옵니다.
읽어온 데이터를 WriteStream으로 pipe를 사용해 써줍니다.
시작 전 메모리 사용량과 파일을 다 쓴 후 메모리 용량을 비교해보겠습니다.
const fs = require("fs");
// Power of Stream
console.log('파일 올리기 전 : ', process.memoryUsage().rss);
const readStream = fs.createReadStream('./bigFile.txt', {highWaterMark : 16000});
const writeStream = fs.createWriteStream('./newBigFile.txt');
readStream.pipe(writeStream);
readStream.on('end', ()=>{
console.log('파일 올린 후 : ', process.memoryUsage().rss);
})
매우 놀랍게도 올리기전 32MB에서 불과 20MB 정도 늘어난 53MB 정도 메모리 사용량이 찍힙니다. 600MB 메모리 사용량에 비하면 비약적인 발전입니다.
'Programming' 카테고리의 다른 글
Node.js fs.access fs.mkdir fs.open fs.rename 사용법 (0) | 2022.09.03 |
---|---|
Node.js readFile vs readFileSync promise file system module 사용법 (0) | 2022.09.03 |
Node.js crypto 비대칭키 대칭키 구현하기 (0) | 2022.09.02 |
Node.js cookie 사용법 (로그인에 쿠키를 사용하면 안되는 이유) (1) | 2022.08.27 |
Mocha Chai framework for testing install (0) | 2022.08.20 |
댓글