쌓고 쌓다

[node] 버퍼와 스트림 / pipe 본문

프로그래밍/node.js

[node] 버퍼와 스트림 / pipe

승민아 2023. 1. 29. 18:59

파일을 읽고 쓸 때 크게 두 가지 방식이 있다.

버퍼를 이용하거나 스트림을 이용하는 방식이다.

 

버퍼

fs 모듈에서 사용한 readFile 메소드 또한 버퍼 방식을 사용한다.

파일을 읽을때 메모리에 파일 크기만큼 공간을 마련한 뒤, 그 공간에 데이터를 저장하고 사용자가 사용한다.

이때 메모리에 저장된 데이터를 버퍼라고 한다. 일정한 크기로 데이터를 모은 것이다.

일정한 크기가 되면 한 번에 데이터를 처리하며, 버퍼링은 버퍼에 데이터가 찰 때까지 모으는 작업을 말한다.

Buffer 클래스로 버퍼를 다룰 수 있다.

출처: Node.js 교과서, 조현영

스트림

버퍼 방식에서 100MB 파일을 읽을 때 100MB 버퍼를 만들어야 했었다. 이게 10개가 되면 1GB가 된다. 메모리의 문제가 발생할 수 있다는 말이다. 게다가 버퍼에 다 써야지 파일 읽기 등 조작이 가능했다.

그래서 스트림 방식이 등장했는데, 버퍼(청크)의 크기를 작게 만들어 여러 번 나눠 보내는 방식이다.

100MB 파일이 있다면 버퍼 1MB를 만들어 100번을 걸쳐 100MB 전체를 보내는 것이다.

1MB 공간 만으로도 100MB 파일 전송이 가능하다.

스트리밍은 일정한 크기의 데이터를 지속적으로 전달하는 작업을 말한다.

파일을 스트림으로 읽는 메소드로 createReadStream이 있다.

출처: Node.js 교과서, 조현영

 

버퍼를 사용하여 1GB 용량의 파일을 복사한다면

복사를 위해 메모리에 1GB 메모리에 파일을 모두 올려둔 뒤, 수행해야 한다.

스트림을 이용하면 작은 메모리를 차지하여 작은 버퍼 단위로 1GB 파일을 전송할 수 있다.


Buffer 사용하기!

Buffer 주요 메소드 및 속성

  • from(문자열) : 문자열을 버퍼로 바꾼다.
  • length : 버퍼의 크기를 반환한다. (바이트 단위)
  • toString(버퍼) : 버퍼를 문자열로 바꾼다. 
  • concat : 배열 안 버퍼들을 하나로 합친다.
  • alloc(바이트) : 빈 버퍼를 생성한다.

buffer.js

const buffer = Buffer.from('버퍼로 바꿔보세요.');

console.log('from():', buffer); 
console.log('length:', buffer.length);
console.log('toString():', buffer.toString());

const arr = [Buffer.from('띄엄 '), Buffer.from('띄엄 '), Buffer.from('띄어쓰기')];
const buffer2 = Buffer.concat(arr);
console.log('concat():', buffer.toString());

const buffer3 = Buffer.alloc(5);
console.log('alloc(): ', buffer3)

실행 결과

 

스트림 사용하기!

1. 파일 읽기

readme.txt

저는 조금씩 조금씩 나눠서 전달됩니다. 나눠진 조각을 chunk라고 부릅니다.

 

createReadStream.js

- 읽기 스트림 생성

const fs = require('fs');
const readStream = fs.createReadStream('./readme.txt', {highWaterMark: 16});
const data = [];

createReadStream으로 읽기 스트림을 만든다.

두 번째 인수는 옵션 객체를 넣는데, highWaterMark 옵션은 버퍼의 크기(바이트)를 정할 수 있다. (기본 64KB)

data에는 여러 번 나눠 읽은 chunk를 넣을 예정입니다.

 

 

- 'data' 이벤트

readStream.on('data', (chunk) => {
    data.push(chunk);
    console.log('data: ', chunk, chunk.length);
})

실행 결과

 

readStream은 이벤트 리스너를 붙여서 사용한다.

'data' 이벤트는 파일을 읽기 시작하면 발생하는 이벤트이다.

16B씩 읽게 했으므로 16B보다 크다면 여러번 발생할 수 있다. 다 읽었다면 'end' 이벤트가 발생한다.

현재 readme.txt의 파일 크기는 99B라 16B 읽기가 6번 발생하고 남은 3B를 읽은 모습이 보인다.

 

 

- 'end' 이벤트

readStream.on('end', () => {
    console.log('end :', Buffer.concat(data).toString());
})

data 배열에는 청크들이 들어있는데 이것들을 concat으로 붙인 뒤, 문자열로 바꿔 출력을 했다.

 

 

- 'error' 이벤트

readStream.on('error', (err) => {
    console.log('err: ', err);
})

파일을 읽는 도중에 에러가 발생하면 'error' 이벤트가 호출된다.

 

 

2. 파일 쓰기

createWriteStream.js

- 쓰기 스트림 생성

const fs = require('fs');
const writeStream = fs.createWriteStream('./writeme.txt');

첫 번째 인수로 출력 파일명을 쓴다. 두 번째 인수는 옵션이 있지만 사용하지 않았다.

 

- 'finish' 이벤트

writeStream.on('finish', () => {
    console.log('파일 쓰기 완료');
})

파일 쓰기가 종료되면 콜백 함수를 실행한다.

 

- write 메소드 / end 메소드

writeStream.write('이 글을 씁니다.\n');
writeStream.write('한 번 더 씁니다.');
writeStream.end();

write 메소드로 넣을 데이터를 작성한다. 이 메소드는 여러 번 호출할 수 있다.

데이터를 작성하고 난 뒤 end 메소드로 종료를 알려야 한다. 이때 'finish' 이벤트가 발생한다.

 

생성된 파일

 

pipe 사용하기!

createReadStream으로 파일을 읽고 그 스트림을 전달받아 createWriteStream으로 파일을 쓸 수 있다. (파일 복사 유사)

이렇게 스트림끼리 연결하는 것을 '파이핑'이라고 한다.

 

readme.txt

저를 읽어서 writeme.txt로 보내주세요.

 

pipe.js

const fs = require('fs');

const readStream = fs.createReadStream('./readme.txt');
const writeStream = fs.createWriteStream('./writeme.txt');
readStream.pipe(writeStream);

미리 읽기 스트림과 쓰기 스트림을 만들어 두 스트림을 pipe 메소드로 연결하면 저절로 데이터가 넘어간다.

따로 on('data')나 writeSteram.write를 하지 않아도 전달된다.

하지만, 노드 8.5 버전이 나오고부터 fs.copyFile() 메소드를 이용해 복사도 가능하다.

생성된 파일

+ pipe는 스트림 사이에 여러 번 연결이 가능하다.

const fs = require('fs');
const zlib = require('zlib');

const readStream = fs.createReadStream('./readme.txt');
const zlibStream = fs.createGzip();
const writeStream = fs.createWriteStream('./readme.txt.gz');
readStream.pipe(zlibStream).pipe(writeStream)

zlib은 노드에서 파일을 압축하는 모듈이다.

zlib의 createGzip 메소드가 스트림을 지원하므로 readStream, writeStream 중간에 파이핑을 할 수 있다.

 

 

 

 

Comments