node.js Part2 section2,3 stream note taking
아직 한국어 번역이 없습니다. 영문 원문을 표시합니다.
솔직히 section2 헷갈려서 주말 지나고 한 번 더 들었다.
stream
- readable/writable
- 조금씩 지속적으로 흘려보내기
- duplex 읽기와 쓰기의 결합 ex) websocket
- transform 정수기 필터와 비슷한 역할 ex) 암호화 및 복호화
- 코드 실행의 이면: 하드웨어의 상태 변화
- 작성 코드 <-> 컴퓨터 내부의 물리적 상태
- CPU안의 코어, CPU는 두뇌 코어는 요리사 8-16명의 요리사 Multi Core
Promise 방식
// =========================================================================
// 1. 프로미스 기반 비동기 방식 (안전하지만 몹시 느림)
// =========================================================================
async function runPromiseVersion() {
console.log("▶️ [1. Promise 방식] 시작...");
console.time("1_Promise_작업시간");
// 커널에 파일 생성 요청을 보내고 번호표(FileHandle)를 발급받음
const fileHandle = await fsPromises.open(TARGET_FILE, "w");
for (let i = 0; i < MAX_ITERATION; i++) {
// [병목 지점] 매번 디스크에 물리적으로 기록될 때까지 메인 셰프(CPU)가 대기함
await fileHandle.write(
`[시스템 로그] 유저 ${i}님의 데이터 처리가 완료되었습니다.\n`,
);
}
await fileHandle.close(); // 사용이 끝난 번호표를 운영체제에 반납 (메모리 누수 방지)
console.timeEnd("1_Promise_작업시간");
console.log("✅ 완료! (안전하지만 시간이 너무 오래 걸려 실무 도입 불가)\n");
}
- main core <-> fs i/o await으로 연결
- 1개의 로그 쓰기 요청이 완료 될 때까지 대기 sequential
- 코어는 다른 비동기 이벤트를 처리할 수 있는 non blocking 상태
- 하지만 for loop의 1만가지 작업을 완료하기 위해 비효율적인 유휴 시간 I/O wait 발생
- 하나의 core가 100만번의 system call을 발생시킴
- 메모리는 안정적인 상태를 유지함.
비동기 콜백 방식
function runCallbackVersion() {
console.log("▶️ [2. Callback 방식] 시작...");
console.time("2_Callback_작업시간");
fs.open(TARGET_FILE, "w", (err, fd) => {
if (err) throw err;
for (let i = 0; i < MAX_ITERATION; i++) {
// await가 없으므로 100만 개의 쓰기 요청을 스레드 풀에 한꺼번에 융단 폭격
// -> 한정된 RAM에 데이터가 순식간에 쌓이며 OOM(메모리 초과) 발생 위험
// -> 커널 레벨에서 쓰기 순서가 보장되지 않아 로그 데이터 오염 발생
fs.write(
fd,
`[시스템 로그] 유저 ${i}님의 데이터 처리가 완료되었습니다.\n`,
(err) => {
if (err) throw err;
},
);
}
console.log(
"⚠️ 100만 개의 콜백을 백그라운드에 던졌습니다. (서버 강제 종료 위험)",
);
});
// 파일이 열리기도 전에, 그리고 쓰기가 끝나기도 전에 타이머가 즉시 종료됨 (무의미한 측정)
console.timeEnd("2_Callback_작업시간");
}
- 메모리 점유율이 치솟음, js heap memory 초과, OOM으로 서버 사망
- 식당에 100만명의 손님 한꺼번에 주문을 한 상태, 하지만 주방의 속도를 고려하지 않음
- race condition이 발생해서 데이터의 무결성을 훼손함
동기방식
function runSyncVersion() {
console.log("▶️ [3. Sync 방식] 시작...");
console.time("3_Sync_작업시간");
// 메인 스레드를 멈춰 세우고(Blocking) 파일 번호표를 받아옴
const fd = fs.openSync(TARGET_FILE, "w");
for (let i = 0; i < MAX_ITERATION; i++) {
// [치명적 문제] 로그가 디스크에 써지는 동안 V8 엔진(메인 스레드)이 그 자리에 얼어붙음
// -> 이 시간 동안 다른 유저는 웹사이트 접속(API 요청) 불가
fs.writeSync(
fd,
`[시스템 로그] 유저 ${i}님의 데이터 처리가 완료되었습니다.\n`,
);
}
fs.closeSync(fd);
console.timeEnd("3_Sync_작업시간");
console.log("✅ 완료! (속도는 빠르나, 실행되는 동안 서버가 완전히 마비됨)\n");
}
- 메인스레드를 완전히 차단함
- File descriptor를 대기없이 즉시 반환
- v8엔진은 정지하고 제어권은 os에 위임
스트림
async function runBadStream() {
console.log("▶️ [4. Bad Stream 방식] 시작...");
console.time("4_BadStream_작업시간");
const fileHandle = await fsPromises.open(TARGET_FILE, "w");
const stream = fileHandle.createWriteStream(); // 16KB 내부 버퍼 생성
for (let i = 0; i < MAX_ITERATION; i++) {
// 디스크가 처리하는 속도를 무시하고 16KB RAM 버퍼에 100만 개를 0.1초 만에 때려 박음
// 내부적으로 false(버퍼 초과)를 반환하지만 무시하고 강행 -> 메모리 터짐
stream.write(`[시스템 로그] 유저 ${i}님의 데이터 처리가 완료되었습니다.\n`);
}
await fileHandle.close()
console.timeEnd("4_BadStream_작업시간");
console.log(
"⚠️ 완료된 것처럼 보이지만, 사실 커널이 백그라운드에서 힘겹게 옮겨 적는 중입니다. (메모리 폭주)\n",
);
}
- await으로 파일을 열고 스트림을 생성 버퍼는 16kb
- for loop내에서 stream.write를 실행
- 버퍼는 메모리 적재하는 공간, stream.write 메모리에 data를 push
- data는 단지 memory에 쏟아 부어졌을 뿐, disk에 적는 작업은 백그라운드에서 실행 중
stream backpressure
async function runOptimalStream() {
console.log("▶️ [5. Optimal Stream (백프레셔 제어) 방식] 시작...");
console.time("5_OptimalStream_작업시간");
// 파일 핸들 획득과 스트림 파이프 생성을 한 번에 깔끔하게 처리 (기본 16KB 버퍼)
const stream = fs.createWriteStream(TARGET_FILE);
for (let i = 0; i < MAX_ITERATION; i++) {
const data = `[시스템 로그] 유저 ${i}님의 데이터 처리가 완료되었습니다.\n`;
// [핵심 흐름 제어] 버퍼 한계치(16KB)를 초과하여 false가 반환(백프레셔 발생)되면
if (!stream.write(data)) {
// 무작정 붓기를 멈추고, 커널이 디스크로 물을 다 빼내어 버퍼가 비워질 때(drain)까지 대기
await new Promise((resolve) => stream.once("drain", resolve));
}
}
// 100만 번의 쓰기가 끝나면 파이프 입구를 잠금 (더 보낼 데이터가 없음을 커널에 알림)
stream.end();
// 파이프에 남은 마지막 잔여 데이터까지 디스크에 안전하게 기록될 때까지 완벽히 대기
await finished(stream);
console.timeEnd("5_OptimalStream_작업시간");
console.log(
"✅ 완벽하게 완료! (메모리 안전, 메인 스레드 논블로킹, 압도적 속도)\n",
);
}
- 완벽한 해결책 조건: 가장 빠르고, 최소 메모리, 논 블로킹
- 스트림의 back pressure를 제어
- stream.write의 true/false 반환을 이용해서 제어
- stream.once 1회만 트리거 되는 event emitter "dreain"신호를 활용
- 메모리는 폭발하지 않고 일정한 최소수위를 유지, main thread도 차단 안됨
streaming data : abstraction interface
- 거대한 데이터는 한번에 이동하지 않고 잘게 분할된다.
stream 4 types
writable stream
- properties, method, event를 가지고 있음
- highwatermakr에 도달하면 systemcall 발생
- 16kb를 한꺼번에 밀어내는 flush
readable stream
- .read() 데이터를 원할 때 가져오는
- .pause() 일시정지
- .resume() 재개
duplex
- 읽기 전용 버퍼 + 쓰기 전용 버퍼
- 쉼 없는 양방향 소통: web socket
transform
- input data -> 가공 및 변형 -> 결과물
- zlib, crypto 모듈