Node.js 스트림 이해하기: 데이터 처리 최적화 비법
Node.js 스트림 이해하기: 데이터 처리 최적화 비법
Node.js는 비동기 I/O(입출력) 작업과 이벤트 기반 처리를 통해 뛰어난 성능을 자랑합니다. 이러한 성능의 핵심 요소 중 하나가 바로 스트림(Stream)
입니다. 스트림은 대용량 데이터를 처리하는 데 있어 매우 중요한 역할을 하며, 데이터가 전송되는 동안 부분적으로 데이터에 접근하고 처리할 수 있게 해줍니다. 이 글에서는 Node.js의 스트림 개념을 이해하고 이를 활용해 효율적인 데이터 처리를 구현하는 방법에 대해 설명합니다.
스트림의 기본 개념
스트림은 데이터의 흐름을 표현하는 추상화된 개념으로, 데이터 조각(chunk)을 순차적으로 처리할 수 있게 도와줍니다. 이를 통해 메모리 효율성을 높일 수 있으며, 입력과 출력 작업 동안 비동기적으로 데이터를 처리할 수 있습니다. Node.js에서 스트림의 종류는 크게 네 가지로 나뉩니다:
- Readable Streams: 읽기 전용 스트림으로, 데이터 소스로부터 데이터 조각을 읽어들입니다.
- Writable Streams: 쓰기 전용 스트림으로, 데이터를 특정 목적지로 쓰는 역할을 합니다.
- Duplex Streams: 양방향 스트림으로, 읽기와 쓰기를 모두 수행할 수 있습니다. 예로는 TCP 소켓이 있습니다.
- Transform Streams: 데이터를 읽고 쓰는 동시에 데이터를 변환하는 스트림입니다.
Readable Stream 사용하기
Readable Stream은 외부 데이터 소스로부터 데이터를 읽어들이는 기능을 합니다. 예를 들어, 파일 시스템에서 파일을 읽는 것을 생각해볼 수 있습니다. Node.js에서는 fs.createReadStream
메서드를 사용하여 파일 시스템의 파일을 읽는 Readable Stream을 생성할 수 있습니다.
javascriptconst fs = require('fs'); const readableStream = fs.createReadStream('example.txt', { encoding: 'utf8', highWaterMark: 16 * 1024 // 16KB 청크 사이즈 }); readableStream.on('data', (chunk) => { console.log('New chunk received:', chunk); }); readableStream.on('end', () => { console.log('No more data to read.'); }); readableStream.on('error', (err) => { console.error('An error occurred:', err.message); });
위 코드에서는 example.txt
파일을 16KB씩 읽어들이는 데모를 보여줍니다. data
이벤트 리스너는 새 데이터 덩어리를 받을 때마다 호출되며, end
이벤트는 모든 데이터를 다 읽었을 때 호출됩니다.
Writable Stream 사용하기
Writable Stream은 데이터를 특정 대상에 기록하는 역할을 합니다. 파일에 데이터를 쓰는 예제를 살펴보겠습니다. 마찬가지로 Node.js의 fs
모듈을 이용합니다.
javascriptconst fs = require('fs'); const writableStream = fs.createWriteStream('output.txt'); writableStream.write('Hello, world! '); writableStream.write('Writing to a file using streams is efficient! '); writableStream.end('This is the end of the stream. '); writableStream.on('finish', () => { console.log('All data written to file.'); }); writableStream.on('error', (err) => { console.error('An error occurred:', err.message); });
이 코드는 output.txt
파일에 데이터를 쓰는 데모입니다. write
메서드를 통해 데이터를 스트림으로 쓰며, end
메서드는 스트림이 더 이상 쓸 데이터가 없음을 나타냅니다.
Duplex 및 Transform Streams 사용하기
Duplex 스트림은 읽기와 쓰기를 동시에 할 수 있는 스트림입니다. 예를 들어 TCP 소켓 서버의 예제를 들어 보겠습니다.
javascriptconst net = require('net'); const server = net.createServer((socket) => { console.log('Client connected'); socket.on('data', (data) => { console.log('Received data:', data.toString()); socket.write('Echo: ' + data); }); socket.on('end', () => { console.log('Client disconnected'); }); socket.on('error', (err) => { console.error('An error occurred:', err.message); }); }); server.listen(8080, () => { console.log('Server listening on port 8080'); });
이 예제에서는 클라이언트와 서버 간의 양방향 데이터 통신을 제공합니다. 클라이언트가 데이터를 보낼 때 서버는 해당 데이터를 받아서 처리하고, 다시 클라이언트로 돌려보내는(에코) 기능을 수행합니다.
Transform 스트림은 읽은 데이터를 변환하여 다시 쓰는 스트림입니다. 예를 들어, 데이터의 내용을 대문자로 변환하는 예제를 보겠습니다.
javascriptconst { Transform } = require('stream'); class UpperCaseTransform extends Transform { _transform(chunk, encoding, callback) { this.push(chunk.toString().toUpperCase()); callback(); } } const uppercase = new UpperCaseTransform(); process.stdin.pipe(uppercase).pipe(process.stdout);
위 코드는 stdin에서 입력된 데이터를 대문자로 변환하여 stdout으로 출력합니다. Transform 스트림은 데이터를 실시간으로 변형하여 처리할 수 있는 매우 강력한 도구입니다.
스트림 사용의 장점
- 메모리 효율성: 스트림을 사용하면 데이터를 작은 청크 단위로 처리할 수 있어 메모리 사용량이 적습니다.
- 비동기 처리: I/O 작업을 비동기로 처리하여 높은 처리 성능을 제공합니다.
- 파이프라인 구성: 스트림을 체인으로 연결하여 데이터 파이프라인을 구성할 수 있습니다.
- 광범위한 활용성: 파일, 네트워크, 소켓, 그리고 다른 I/O 소스에 대해 쉽게 사용할 수 있습니다.
결론
Node.js 스트림은 대용량 데이터를 효율적으로 처리할 수 있는 강력한 도구입니다. 기본적인 개념을 이해하고 다양한 스트림을 활용하면 메모리 효율적이고 성능 좋은 애플리케이션을 개발할 수 있습니다. 본문에서 제공한 예제들을 통해 직접 스트림의 기능을 실습해 보세요.