Buffer와 Stream은 대용량 데이터를 효율적으로 처리하기 위한 중요한 개념입니다. 두 개념 모두 파일, 네트워크, 데이터베이스와 같은 외부 자원으로부터 데이터를 처리할 때 자주 사용됩니다.
Buffer
는 효율적인 이진 데이터 처리를 위한 수단을 제공합니다.UTF-8
, Base64
, Hex
등의 인코딩을 지원합니다.Buffer
객체는 Node.js가 기본 제공하며, require()
를 통해 별도의 모듈을 로드하지 않아도 됩니다.Buffer.from()
: 문자열, 배열 등으로부터 Buffer
객체를 생성합니다.Buffer.toString()
: Buffer
객체를 다시 문자열로 변환합니다.Buffer.length
: 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 (바이트 단위)
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)
스트림 처리 중에 오류가 발생하면 호출됩니다. 스트림에서 문제가 생겼을 때 처리할 수 있도록 예외를 처리하는 중요한 메서드입니다. Readable 및 Writable 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('스트림 쓰기 종료');
});
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);
});
pipe()
는 Readable Stream에서 Writable Stream으로 데이터를 연결하는 메서드입니다. 예를 들어, 파일을 읽은 후 다른 파일에 쓰거나, 파일에서 읽은 데이터를 네트워크에 전송하는 경우에 사용할 수 있습니다.
pipe()
의 장점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
결과
pipe()
메서드를 사용하면 자동으로 백프레셔가 처리되므로, 읽기와 쓰기 속도가 다를 때도 데이터의 흐름이 관리됩니다. 결과적으로 Stream 방식에서는 메모리 사용량이 일정하게 유지되며, 전체 데이터를 메모리에 적재하지 않기 때문에 약 11MB만을 사용했습니다.