[Node.js] Buffer와 Stream

Main·2024년 9월 28일
0

Node.js

목록 보기
8/20
post-thumbnail

BufferStream은 대용량 데이터를 효율적으로 처리하기 위한 중요한 개념입니다. 두 개념 모두 파일, 네트워크, 데이터베이스와 같은 외부 자원으로부터 데이터를 처리할 때 자주 사용됩니다.


Buffer

Buffer 개념

  • Buffer고정 크기의 메모리 공간을 사용하여 이진 데이터(바이너리 데이터)를 저장하는 객체입니다.
  • Node.js는 파일 시스템, 네트워크 요청 등에서 텍스트 뿐만 아니라 이진 데이터를 처리할 필요가 있습니다. 이때, Buffer는 효율적인 이진 데이터 처리를 위한 수단을 제공합니다.
  • 특히 Stream과 같이 데이터를 작은 덩어리로 나누어 처리할 때 유용합니다.

Buffer 특징

  • 고정된 크기를 가지며, 초기화 시 크기를 정해야 합니다.
  • Raw binary data(원시 이진 데이터)를 처리할 수 있으며, UTF-8, Base64, Hex 등의 인코딩을 지원합니다.
  • Buffer 객체는 Node.js가 기본 제공하며, require()를 통해 별도의 모듈을 로드하지 않아도 됩니다.

Buffer 주요 메서드

  • Buffer.from(): 문자열, 배열 등으로부터 Buffer 객체를 생성합니다.
  • Buffer.toString(): Buffer 객체를 다시 문자열로 변환합니다.
  • Buffer.length: Buffer의 길이를 바이트 단위로 반환합니다.

Buffer 예시 코드

// Buffer 생성: 'Hello' 문자열을 Buffer로 변환
const buf = Buffer.from('Hello', 'utf8');

console.log(buf);               // <Buffer 48 65 6c 6c 6f> (16진수로 출력)
console.log(buf.toString());    // Hello (UTF-8로 다시 문자열로 변환)

// Buffer의 길이 출력
console.log(buf.length);        // 5 (바이트 단위)

Stream

Stream 개념

  • Stream연속적인 데이터 흐름을 처리하는 방식입니다.
  • Stream을 사용하면 대용량 데이터를 작은 청크(chunk) 단위로 나누어 처리할 수 있습니다. 이로 인해 메모리를 효율적으로 사용하면서 데이터 전송 및 처리가 가능합니다.
  • 파일을 읽거나 쓰는 작업, 네트워크 요청 처리, 데이터베이스와의 연결, 압축 및 암호화 등의 작업에서 자주 사용됩니다.

Stream의 4가지 유형

  • Writable Stream: 쓰기 작업을 지원하는 스트림. 데이터를 쓸 수 있습니다.
  • Readable Stream: 읽기 작업을 지원하는 스트림. 데이터를 읽을 수 있습니다.
  • Duplex Stream: 읽기와 쓰기 모두 가능한 스트림.
  • Transform Stream: 읽은 데이터를 변환하고, 변환된 데이터를 쓰는 스트림.

Stream의 장점

  • 메모리 효율성: 대용량 데이터를 한 번에 처리하는 대신, 작은 덩어리로 나누어 처리하므로 메모리 사용량이 적습니다.
  • 성능 최적화: 데이터를 스트리밍 방식으로 처리하면서 읽고 쓰는 작업을 동시에 할 수 있습니다.

Stream 주요 메서드

1 ) stream.on('data', callback)

스트림에서 데이터를 읽을 때마다 호출됩니다. Readable Stream에서 주로 사용되며, 스트림이 데이터를 청크(chunk) 단위로 읽어올 때마다 이 이벤트가 발생합니다. callback 함수의 매개변수로는 읽어들인 데이터 청크가 전달됩니다.

readableStream.on('data', (chunk) => {
  console.log('데이터 청크:', chunk);
});

2 ) stream.on('end', callback)
스트림에서 데이터를 모두 읽고 나면 호출됩니다. Readable Stream에서 데이터가 끝났을 때 실행됩니다. 파일을 전부 읽거나 네트워크 응답이 끝났을 때 주로 사용됩니다.

readableStream.on('end', () => {
  console.log('스트림 읽기 완료');
});

3 ) stream.on('error', callback)
스트림 처리 중에 오류가 발생하면 호출됩니다. 스트림에서 문제가 생겼을 때 처리할 수 있도록 예외를 처리하는 중요한 메서드입니다. ReadableWritable Stream 모두에서 사용할 수 있습니다.

stream.on('error', (err) => {
  console.error('스트림에서 오류 발생:', err);
});

4 ) stream.on('drain', callback)

Writable Stream에서 내부 버퍼가 꽉 찬 후 다시 데이터를 쓸 수 있을 때 발생하는 이벤트입니다. 일반적으로 stream.write()false를 반환하면 내부 버퍼가 가득 찬 것이므로, drain 이벤트가 발생할 때까지 새로운 데이터를 쓰지 말아야 합니다. 버퍼가 비워지면 drain 이벤트가 발생하고, 이때 데이터를 다시 쓸 수 있게 됩니다.

drain 이벤트는 백프레셔(backpressure) 문제를 해결하기 위한 중요한 메커니즘입니다. 데이터를 너무 빨리 스트림에 써서 버퍼가 가득 차는 상황을 방지하며, 메모리 사용을 효율적으로 관리할 수 있습니다.

writableStream.on('drain', () => {
  console.log('drain 이벤트 발생! 스트림에 데이터를 다시 쓸 수 있습니다.');
  // 추가 데이터 쓰기 작업을 진행할 수 있음
});

5 ) stream.write(data, [encoding], callback)

Writable Stream에 데이터를 씁니다. data는 쓰고자 하는 내용이며, encoding은 해당 데이터를 문자열로 쓸 때 사용할 인코딩 방식을 지정할 수 있습니다. callback은 데이터가 성공적으로 쓰인 후 호출되는 함수입니다.

writableStream.write('데이터 내용\n', 'utf8', () => {
  console.log('데이터가 성공적으로 쓰였습니다.');
});

6 ) stream.end([data], [encoding], callback)
쓰기를 종료하고 스트림을 닫을 때 사용합니다. 필요한 경우 마지막 데이터를 쓰고 스트림을 종료할 수 있습니다. Writable Stream이 완료되었을 때 호출됩니다.

writableStream.end('마지막 데이터\n', 'utf8', () => {
  console.log('스트림 쓰기 종료');
});

Stream 예시 코드 (파일 쓰기, 읽기)

Writable Stream 사용 예시

const fs = require('fs');

// 파일을 쓰기 위한 Writable Stream 생성
const writableStream = fs.createWriteStream('output.txt');

writableStream.write('첫 번째 라인\n', 'utf8');
writableStream.write('두 번째 라인\n', 'utf8');

// 스트림 끝내기
writableStream.end();

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

writableStream.on('error', (err) => {
    console.error('파일 쓰기 중 오류 발생:', err);
});

Readable Stream 사용 예시

const fs = require('fs');

const data = [];
// 파일을 읽기 위한 Readable Stream 생성
const readableStream = fs.createReadStream('output.txt', {
    highWaterMark: 16  // 버퍼 크기 (청크 단위) 설정, 16바이트
});

readableStream.on('data', (chunk) => {
    console.log('새로운 청크 수신:', chunk, chunk.length);
		data.push(chunk);
});

readableStream.on('end', () => {
    console.log('파일 읽기 완료');
		console.log(Buffer.concat(data).toString());
});

readableStream.on('error', (err) => {
    console.error('파일 읽기 중 오류 발생:', err);
});

Buffer와 Stream의 관계

  • Buffer는 데이터를 임시로 저장하는 작은 메모리 공간입니다. Stream은 데이터를 연속적으로 처리하면서 내부적으로 Buffer를 사용하여 데이터를 한 번에 처리하지 않고 작은 청크 단위로 처리합니다.
  • 예를 들어, 파일을 읽을 때 Stream을 사용하면, 파일 전체를 메모리에 로드하는 대신 작은 Buffer 청크 단위로 읽어서 처리합니다. 이는 메모리 사용을 절약하고 처리 속도를 개선합니다.

Buffer와 Stream의 차이점

  • Buffer는 메모리에 로드된 고정 크기의 데이터를 처리하며, 전체 데이터를 메모리에 올려 놓고 한 번에 처리할 때 사용됩니다.
  • Stream은 데이터를 청크 단위로 처리하며, 큰 파일이나 실시간 데이터를 처리할 때 유리합니다. Stream은 메모리에 전체 데이터를 로드하지 않으므로 더 큰 파일을 처리할 수 있습니다.

Pipe()

pipe()Readable Stream에서 Writable Stream으로 데이터를 연결하는 메서드입니다. 예를 들어, 파일을 읽은 후 다른 파일에 쓰거나, 파일에서 읽은 데이터를 네트워크에 전송하는 경우에 사용할 수 있습니다.


pipe()의 장점

  • 메모리 효율성: 데이터가 한 번에 처리되지 않고 청크(chunk) 단위로 처리됩니다. 대용량 파일도 메모리 전체를 사용하지 않으므로, 메모리 부담이 적습니다.
  • 자동 흐름 제어: pipe()백프레셔(backpressure) 메커니즘을 사용하여 읽기 속도쓰기 속도가 다를 때 자동으로 조절합니다. 이를 통해 데이터가 지나치게 빨리 전달되지 않도록 하여 메모리 오버플로우를 방지합니다.
  • 코드 간소화: 데이터의 흐름을 수동으로 관리할 필요 없이, pipe()를 사용하면 Stream 간의 데이터를 쉽게 전달할 수 있습니다.

파일 복사 예시 코드

  • readableStream.pipe(writableStream) : input.txt 파일에서 데이터를 읽고 output.txt 파일에 씁니다. 데이터가 청크 단위로 읽히고 쓰여서 메모리 사용이 최소화됩니다.
  • pipe()는 데이터를 청크 단위로 처리하므로, 대용량 파일을 처리할 때도 전체 파일을 메모리에 올리지 않고 부분적으로 처리할 수 있습니다.
const fs = require('fs');

// Readable Stream 생성 (파일 읽기)
const readableStream = fs.createReadStream('input.txt');

// Writable Stream 생성 (파일 쓰기)
const writableStream = fs.createWriteStream('output.txt');

// pipe()로 스트림 연결 (input.txt의 내용을 output.txt로 복사)
readableStream.pipe(writableStream);

writableStream.on('finish', () => {
    console.log('파일 복사 완료');
});

writableStream.on('error', (err) => {
    console.error('파일 쓰기 중 오류 발생:', err);
});

대용량 파일 생성 예시코드

Buffer 방식

const fs = require('fs');

let data = '';
console.log("before: ",process.memoryUsage().rss);  // 파일 생성 전 메모리 사용량 출력

for (let i = 0; i <= 10000000; i++) {
  data += '안녕하세요. 큰 용량의 파일입니다.\n';
}

console.log("after: ",process.memoryUsage().rss);  // 파일 생성 후 메모리 사용량 출력

// 데이터를 한 번에 파일로 쓰기
fs.writeFileSync('largeFile_buffer.txt', data, 'utf8');

파일 생성 전 메모리 : 26,238,976 Byte

파일 생성 후 메모리 : 382,238,720 Byte

사용 메모리 : 355,999,748 Byte

Stream 방식

const fs = require("fs");

// 파일 생성 전 메모리 사용량 출력
console.log("before: ", process.memoryUsage().rss); 

// Read Stream 생성
const readStream = fs.createReadStream("./largeFile_buffer.txt");
// Write Stream 생성
const writeStream = fs.createWriteStream("largeFile_stream.txt");

// pipe를 통해 파일 복사
readStream.pipe(writeStream);

readStream.on('end',() => {
  // 파일 생성 후 메모리 사용량 출력
  console.log("after: ", process.memoryUsage().rss); 
});

파일 생성 전 메모리 : 26,251,264 Byte

파일 생성 후 메모리 : 37,928,960 Byte

사용 메모리 : 11,677,696 Byte

결과

  • Buffer 방식 : 전체 데이터를 메모리에 한 번에 적재한 후, 그 데이터를 파일로 쓰는 방식입니다. 이는 작은 데이터의 경우 문제가 없지만, 대용량 파일을 처리할 때는 메모리 사용량이 매우 커질 수 있습니다. 위의 예시에서 10,000,000번의 반복을 통해 문자열을 메모리에 적재한 결과, 메모리 사용량이 약 355MB 증가했습니다.
  • Stream 방식 : 데이터를 작은 청크(chunk) 단위로 읽고 쓰기 때문에, 메모리를 효율적으로 사용할 수 있습니다. pipe() 메서드를 사용하면 자동으로 백프레셔가 처리되므로, 읽기와 쓰기 속도가 다를 때도 데이터의 흐름이 관리됩니다. 결과적으로 Stream 방식에서는 메모리 사용량이 일정하게 유지되며, 전체 데이터를 메모리에 적재하지 않기 때문에 약 11MB만을 사용했습니다.
profile
함께 개선하는 프론트엔드 개발자

0개의 댓글