diff --git a/chapter11/kilhyeonjun/README.md b/chapter11/kilhyeonjun/README.md new file mode 100644 index 0000000..2f32b7f --- /dev/null +++ b/chapter11/kilhyeonjun/README.md @@ -0,0 +1,432 @@ +# Chapter 11: 고급 레시피 + +> **발표자**: 길현준 +> **발표일**: 2025-12-08 +> **주제**: 비동기 초기화, 요청 배치/캐싱, 작업 취소, CPU 바운드 실행 + +--- + +## 📌 목차 + +1. [개요](#개요) +2. [비동기적으로 초기화되는 컴포넌트 다루기](#1-비동기적으로-초기화되는-컴포넌트-다루기) +3. [비동기식 요청 일괄 처리 및 캐싱](#2-비동기식-요청-일괄-처리-및-캐싱) +4. [비동기 작업 취소](#3-비동기-작업-취소) +5. [CPU 바운드 작업 실행](#4-cpu-바운드-작업-실행) +6. [요약](#요약) +7. [연습문제](#연습문제) + +--- + +## 개요 + +### 왜 중요한가? + +Node.js 애플리케이션을 개발하다 보면 단순한 비동기 처리를 넘어서는 고급 기법이 필요한 상황을 자주 마주합니다: + +- **비동기 초기화**: DB 연결, 설정 로드 등 비동기로 초기화되는 컴포넌트를 어떻게 안전하게 사용할까? +- **중복 요청 최적화**: 동일한 API를 동시에 여러 번 호출할 때 어떻게 효율적으로 처리할까? +- **작업 취소**: 사용자가 페이지를 떠났는데 진행 중인 작업을 어떻게 중단할까? +- **CPU 집약적 작업**: 무거운 계산이 서버 응답을 막는 것을 어떻게 방지할까? + +### 핵심 키워드 + +| 개념 | 설명 | +|------|------| +| **사전 초기화 큐** | 초기화 완료 전 요청을 큐에 저장하여 나중에 실행 | +| **요청 배치** | 동일한 요청 중복 방지 (피기백) | +| **요청 캐싱** | 결과를 저장하여 재사용 | +| **취소 토큰** | 비동기 작업을 안전하게 중단 | +| **작업자 풀** | CPU 작업을 별도 프로세스/스레드에서 실행 | + +--- + +## 1. 비동기적으로 초기화되는 컴포넌트 다루기 + +### 문제 상황 + +```javascript +// ❌ 문제: 연결 전에 쿼리하면 에러 발생 +const db = new DB() +db.connect() // 비동기 초기화 시작 +db.query('SELECT * FROM users') // Error: Not connected yet +``` + +### 해결책 비교 + +#### 1) 로컬 초기화 확인 + +매번 연결 상태를 확인하고 대기합니다. + +```javascript +async function updateLastAccess() { + // 매번 연결 상태 확인 + if (!db.connected) { + await once(db, 'connected') + } + await db.query('INSERT INTO ...') +} +``` + +**단점**: 코드 중복, 모든 함수에서 확인 필요 + +#### 2) 지연 시작 + +모든 초기화가 완료된 후 애플리케이션 로직을 실행합니다. + +```javascript +async function main() { + // 먼저 모든 초기화 완료 대기 + await initialize() + + // 이후 비즈니스 로직 실행 + startServer() +} +``` + +**단점**: 시작 시간 지연, 재초기화 미고려 + +#### 3) 사전 초기화 큐 (권장) + +초기화 전 요청을 큐에 저장하고, 완료 후 일괄 실행합니다. + +```javascript +async query(queryString) { + if (!this.connected) { + // 명령을 큐에 저장하고 Promise 반환 + return new Promise((resolve, reject) => { + this.commandsQueue.push(() => { + this.query(queryString).then(resolve, reject) + }) + }) + } + // 실제 쿼리 실행 + return this._executeQuery(queryString) +} +``` + +### 상태 패턴으로 개선 + +``` +┌──────────────────────────────────────────────────────────┐ +│ Client │ +└──────────────────────────────────────────────────────────┘ + │ + ▼ + ┌─────────────────────────┐ + │ DB │ + │ (Context) │ + │ │ + │ state.query(...) │ + └─────────────────────────┘ + │ + ┌────────────────┼────────────────┐ + │ │ + ▼ ▼ +┌─────────────────────┐ ┌─────────────────────┐ +│ QueuingState │ │ InitializedState │ +│ │ │ │ +│ - 요청을 큐에 저장 │ ──────> │ - 실제 쿼리 실행 │ +│ - Promise 반환 │ connect │ - 비즈니스 로직 │ +└─────────────────────┘ └─────────────────────┘ +``` + +**실전 예시**: Mongoose, pg 라이브러리가 이 방식 사용 + +--- + +## 2. 비동기식 요청 일괄 처리 및 캐싱 + +### 요청 배치 (Batching) + +동일한 API 요청이 진행 중일 때, 새 요청을 시작하지 않고 기존 요청에 **피기백(piggyback)** 합니다. + +``` +┌─────────────────────────────────────────────────────────┐ +│ Without Batching │ +├─────────────────────────────────────────────────────────┤ +│ Client A ──→ [API Call] ──→ Result A │ +│ Client B ──→ [API Call] ──→ Result B │ +│ Client C ──→ [API Call] ──→ Result C │ +│ │ +│ → 3번의 API 호출, 3배의 시간 │ +└─────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────┐ +│ With Batching │ +├─────────────────────────────────────────────────────────┤ +│ Client A ─┐ │ +│ ├──→ [API Call] ──→ Result (공유) │ +│ Client B ─┤ ↓ │ +│ │ A, B, C 모두 동일 결과 │ +│ Client C ─┘ │ +│ │ +│ → 1번의 API 호출, 1/3 시간 │ +└─────────────────────────────────────────────────────────┘ +``` + +```javascript +function createBatchedApi(originalApi) { + const runningRequests = new Map() + + return async function(key) { + // 동일한 요청이 진행 중이면 해당 Promise 반환 + if (runningRequests.has(key)) { + return runningRequests.get(key) + } + + // 새 요청 시작 + const resultPromise = originalApi(key) + runningRequests.set(key, resultPromise) + + resultPromise.finally(() => { + runningRequests.delete(key) + }) + + return resultPromise + } +} +``` + +### 요청 캐싱 (Caching) + +배치 처리에 **TTL(Time To Live)** 기반 캐시를 추가합니다. + +```javascript +function createCachedApi(originalApi, ttlMs = 5000) { + const cache = new Map() + const runningRequests = new Map() + + return async function(key) { + // 1. 캐시 확인 + const cached = cache.get(key) + if (cached && (Date.now() - cached.timestamp) < ttlMs) { + return cached.value // Cache HIT + } + + // 2. 배치 확인 + if (runningRequests.has(key)) { + return runningRequests.get(key) + } + + // 3. 새 요청 + const resultPromise = originalApi(key) + runningRequests.set(key, resultPromise) + + const result = await resultPromise + cache.set(key, { value: result, timestamp: Date.now() }) + runningRequests.delete(key) + + return result + } +} +``` + +### 캐싱 고려사항 + +| 고려사항 | 설명 | +|----------|------| +| **TTL 설정** | 데이터 특성에 맞는 적절한 만료 시간 | +| **메모리 관리** | LRU 알고리즘으로 오래된 항목 제거 | +| **무효화 전략** | 데이터 변경 시 캐시 동기화 | +| **분산 캐시** | 여러 서버 간 캐시 공유 (Redis 등) | + +--- + +## 3. 비동기 작업 취소 + +### 기본 패턴: cancelRequested 플래그 + +```javascript +async function cancellableOperation(cancelObj) { + const res1 = await asyncStep('Step 1') + if (cancelObj.cancelRequested) { + throw new CancelError() + } + + const res2 = await asyncStep('Step 2') + if (cancelObj.cancelRequested) { + throw new CancelError() + } + + return [res1, res2] +} + +// 사용 +const cancelObj = { cancelRequested: false } +const promise = cancellableOperation(cancelObj) + +setTimeout(() => { + cancelObj.cancelRequested = true // 취소 요청 +}, 500) +``` + +### 래퍼 패턴 + +```javascript +function createCancelWrapper() { + let cancelRequested = false + + return { + cancel: () => { cancelRequested = true }, + cancelWrapper: (asyncFn, ...args) => { + if (cancelRequested) { + return Promise.reject(new CancelError()) + } + return asyncFn(...args) + } + } +} +``` + +### 제너레이터 패턴 (권장) + +```javascript +const cancellableOperation = createAsyncCancelable(function* () { + // 각 yield가 자동으로 취소 포인트 + const resA = yield asyncStep('Step A') + const resB = yield asyncStep('Step B') + const resC = yield asyncStep('Step C') + return [resA, resB, resC] +}) + +// 사용 +const { promise, cancel } = cancellableOperation() +setTimeout(cancel, 500) // 500ms 후 취소 +await promise +``` + +**참고 라이브러리**: [caf](https://github.com/getify/CAF) + +--- + +## 4. CPU 바운드 작업 실행 + +### 문제: 이벤트 루프 차단 + +```javascript +// ❌ 무거운 동기 계산 - 서버가 응답 불가 +function heavyComputation(data) { + // O(2^n) 시간 복잡도... + for (let i = 0; i < 1e10; i++) { /* ... */ } +} +``` + +### 해결책 비교 + +``` +┌────────────────────────────────────────────────────────────────┐ +│ Main Thread │ +│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ +│ │ Request │ │ Request │ │ Request │ │ Request │ │ +│ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │ +│ │ │ │ │ │ +│ ▼ ▼ ▼ ▼ │ +│ ┌──────────────────────────────────────────────────┐ │ +│ │ Event Loop (Non-Blocking) │ │ +│ └──────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌────────────────────┼────────────────────┐ │ +│ ▼ ▼ ▼ │ +│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ +│ │Worker 1 │ │Worker 2 │ │Worker 3 │ │ +│ │(Thread) │ │(Thread) │ │(Process)│ │ +│ └─────────┘ └─────────┘ └─────────┘ │ +└────────────────────────────────────────────────────────────────┘ +``` + +#### 1) setImmediate 인터리빙 + +```javascript +// 계산 단계 사이에 이벤트 루프에 양보 +function* compute() { + for (let i = 0; i < items.length; i++) { + yield processItem(items[i]) // 각 단계 후 양보 + } +} +``` + +**장점**: 추가 리소스 불필요 +**단점**: 성능 오버헤드, 단일 코어만 사용 + +#### 2) 외부 프로세스 (child_process.fork) + +```javascript +const pool = new ProcessPool('./worker.js', 4) +const result = await pool.run({ set, sum }) +``` + +**장점**: 멀티코어 활용, 크래시 격리 +**단점**: 메모리 사용량, 시작 비용 + +#### 3) 작업자 스레드 (worker_threads) + +```javascript +const pool = new ThreadPool(4) +const result = await pool.run({ set, sum }) +``` + +**장점**: 프로세스보다 가벼움, SharedArrayBuffer 지원 +**단점**: Node 10.5+ 필요 + +### 비교표 + +| 방법 | 이벤트 루프 | 멀티코어 | 오버헤드 | 적합한 경우 | +|------|-------------|----------|----------|-------------| +| setImmediate | 공유 | ❌ | 낮음 | 짧은 단계로 분할 가능한 작업 | +| child_process | 분리 | ✅ | 높음 | 장시간 실행, 격리 필요 | +| worker_threads | 분리 | ✅ | 중간 | 빈번한 CPU 작업, 메모리 공유 | + +**권장 라이브러리**: +- [workerpool](https://github.com/josdejong/workerpool) +- [piscina](https://github.com/piscinajs/piscina) + +--- + +## 요약 + +| 패턴 | 문제 상황 | 해결 방법 | 실전 예시 | +|------|----------|----------|----------| +| **사전 초기화 큐** | 비동기 초기화 전 API 호출 | 요청을 큐에 저장 후 나중에 실행 | Mongoose, pg | +| **요청 배치** | 동일 API 중복 호출 | runningRequests Map으로 피기백 | API Gateway | +| **요청 캐싱** | 반복적인 동일 요청 | TTL 기반 캐시 + 배치 | Redis 캐시 | +| **작업 취소** | 불필요해진 진행 중 작업 | cancelWrapper, 제너레이터 | caf | +| **CPU 바운드** | 이벤트 루프 차단 | 프로세스/스레드 풀 | piscina | + +--- + +## 연습문제 + +### 11.1 프록시를 사용한 대기열 구현 + +Proxy를 사용하여 비동기로 초기화되는 모든 컴포넌트에 대기열을 투명하게 적용하는 일반 래퍼 만들기. + +### 11.2 콜백 기반 배치 및 캐싱 + +Promise 없이 콜백 방식으로 요청 배치 및 캐싱 구현하기. + +### 11.3 Deep 취소 가능한 비동기 함수 + +중첩된 취소 가능 함수에서 루트 함수 취소 시 모든 중첩 함수까지 취소되는 기능 구현하기. + +### 11.4 컴퓨팅 팜 + +HTTP로 작업을 분산하고 eval/vm으로 동적 코드를 실행하는 분산 컴퓨팅 시스템 구현하기. + +--- + +## 참고 자료 + +### 라이브러리 + +- [Mongoose](https://mongoosejs.com/) - 사전 초기화 큐 사용 +- [pg](https://node-postgres.com/) - PostgreSQL 클라이언트 +- [caf](https://github.com/getify/CAF) - 취소 가능한 비동기 함수 +- [workerpool](https://github.com/josdejong/workerpool) - 프로세스/스레드 풀 +- [piscina](https://github.com/piscinajs/piscina) - 작업자 스레드 풀 + +### 공식 문서 + +- [child_process](https://nodejs.org/api/child_process.html) +- [worker_threads](https://nodejs.org/api/worker_threads.html) +- [SharedArrayBuffer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer) diff --git a/chapter11/kilhyeonjun/code/01-db-without-queue.js b/chapter11/kilhyeonjun/code/01-db-without-queue.js new file mode 100644 index 0000000..0d93c03 --- /dev/null +++ b/chapter11/kilhyeonjun/code/01-db-without-queue.js @@ -0,0 +1,56 @@ +/** + * 01-db-without-queue.js + * 비동기 초기화 컴포넌트의 문제점 + * + * 연결이 완료되기 전에 쿼리를 실행하면 에러 발생 + */ + +import { EventEmitter } from 'events' + +// 비동기 초기화가 필요한 DB 모듈 +class DB extends EventEmitter { + connected = false + + connect() { + console.log('Connecting to database...') + // 연결 지연 시뮬레이션 (500ms) + setTimeout(() => { + this.connected = true + console.log('Database connected!') + this.emit('connected') + }, 500) + } + + async query(queryString) { + if (!this.connected) { + throw new Error('Not connected yet') + } + console.log(`Query executed: ${queryString}`) + return { rows: [] } + } +} + +export const db = new DB() + +// 문제 시연 +async function main() { + db.connect() + + // 연결 완료 전에 쿼리 실행 시도 - 에러 발생! + try { + await db.query('SELECT * FROM users') + } catch (err) { + console.error('Error:', err.message) + } + + // 600ms 후 (연결 완료 후) 쿼리 실행 - 성공 + setTimeout(async () => { + try { + await db.query('SELECT * FROM users') + } catch (err) { + console.error('Error:', err.message) + } + }, 600) +} + +main() diff --git a/chapter11/kilhyeonjun/code/02-db-local-init.js b/chapter11/kilhyeonjun/code/02-db-local-init.js new file mode 100644 index 0000000..5a57258 --- /dev/null +++ b/chapter11/kilhyeonjun/code/02-db-local-init.js @@ -0,0 +1,63 @@ +/** + * 02-db-local-init.js + * 로컬 초기화 확인 방식 + * + * API 호출 시마다 초기화 여부를 확인하고 대기 + */ + +import { EventEmitter, once } from 'events' + +class DB extends EventEmitter { + connected = false + + connect() { + console.log('Connecting to database...') + setTimeout(() => { + this.connected = true + console.log('Database connected!') + this.emit('connected') + }, 500) + } + + async query(queryString) { + if (!this.connected) { + throw new Error('Not connected yet') + } + console.log(`Query executed: ${queryString}`) + return { rows: [] } + } +} + +export const db = new DB() + +// 로컬 초기화 확인 방식 +async function updateLastAccess() { + // 매번 연결 상태 확인 + if (!db.connected) { + console.log('Waiting for connection...') + await once(db, 'connected') + } + + await db.query(`INSERT (${Date.now()}) INTO "LastAccesses"`) +} + +async function main() { + db.connect() + + // 연결 전 호출 - 대기 후 실행 + updateLastAccess() + + // 600ms 후 호출 - 바로 실행 + setTimeout(() => { + updateLastAccess() + }, 600) +} + +main() + +/** + * 단점: + * - 매번 초기화 상태를 확인해야 함 + * - 코드 중복 발생 + * - 여러 비동기 컴포넌트가 있으면 복잡해짐 + */ diff --git a/chapter11/kilhyeonjun/code/03-db-delayed-startup.js b/chapter11/kilhyeonjun/code/03-db-delayed-startup.js new file mode 100644 index 0000000..6de02c1 --- /dev/null +++ b/chapter11/kilhyeonjun/code/03-db-delayed-startup.js @@ -0,0 +1,68 @@ +/** + * 03-db-delayed-startup.js + * 지연 시작 방식 + * + * 모든 초기화가 완료된 후 애플리케이션 로직 실행 + */ + +import { EventEmitter, once } from 'events' + +class DB extends EventEmitter { + connected = false + + connect() { + console.log('Connecting to database...') + setTimeout(() => { + this.connected = true + console.log('Database connected!') + this.emit('connected') + }, 500) + } + + async query(queryString) { + if (!this.connected) { + throw new Error('Not connected yet') + } + console.log(`Query executed: ${queryString}`) + return { rows: [] } + } +} + +export const db = new DB() + +// 초기화 함수 +async function initialize() { + db.connect() + await once(db, 'connected') + console.log('All services initialized!') +} + +// 비즈니스 로직 (초기화 상태 확인 불필요) +async function updateLastAccess() { + await db.query(`INSERT (${Date.now()}) INTO "LastAccesses"`) +} + +async function main() { + // 먼저 모든 초기화 완료 대기 + await initialize() + + // 이후 비즈니스 로직 실행 + updateLastAccess() + + setTimeout(() => { + updateLastAccess() + }, 100) +} + +main() + +/** + * 장점: + * - 비즈니스 로직에서 초기화 상태 확인 불필요 + * - 간단하고 명확함 + * + * 단점: + * - 애플리케이션 시작 시간 지연 + * - 비동기 컴포넌트의 재초기화는 고려하지 않음 + * - 어떤 컴포넌트가 초기화가 필요한지 미리 알아야 함 + */ diff --git a/chapter11/kilhyeonjun/code/04-db-preinitialization-queue.js b/chapter11/kilhyeonjun/code/04-db-preinitialization-queue.js new file mode 100644 index 0000000..c817811 --- /dev/null +++ b/chapter11/kilhyeonjun/code/04-db-preinitialization-queue.js @@ -0,0 +1,73 @@ +/** + * 04-db-preinitialization-queue.js + * 사전 초기화 큐 방식 + * + * 초기화 전 요청을 큐에 저장하고, 초기화 완료 후 일괄 실행 + */ + +import { EventEmitter } from 'events' + +class DB extends EventEmitter { + connected = false + commandsQueue = [] + + connect() { + console.log('Connecting to database...') + setTimeout(() => { + this.connected = true + console.log('Database connected!') + this.emit('connected') + + // 큐에 쌓인 명령들 실행 + this.commandsQueue.forEach(command => command()) + this.commandsQueue = [] + }, 500) + } + + async query(queryString) { + if (!this.connected) { + console.log(`Request queued: ${queryString}`) + + // 명령을 큐에 저장하고 Promise 반환 + return new Promise((resolve, reject) => { + const command = () => { + this.query(queryString) + .then(resolve, reject) + } + this.commandsQueue.push(command) + }) + } + + console.log(`Query executed: ${queryString}`) + return { rows: [] } + } +} + +export const db = new DB() + +async function main() { + db.connect() + + // 연결 전 호출 - 큐에 저장됨 + const promise1 = db.query('SELECT * FROM users') + const promise2 = db.query('SELECT * FROM orders') + + // 연결 완료 후 자동 실행 + const results = await Promise.all([promise1, promise2]) + console.log('All queries completed!') + + // 이후 호출 - 바로 실행 + setTimeout(async () => { + await db.query('SELECT * FROM products') + }, 600) +} + +main() + +/** + * 장점: + * - 사용자 코드에서 초기화 상태 확인 불필요 + * - 투명한 사용 가능 (초기화 상태를 몰라도 됨) + * + * 이 방식이 Mongoose, pg 등에서 사용됨 + */ diff --git a/chapter11/kilhyeonjun/code/05-db-state-pattern.js b/chapter11/kilhyeonjun/code/05-db-state-pattern.js new file mode 100644 index 0000000..213f2b7 --- /dev/null +++ b/chapter11/kilhyeonjun/code/05-db-state-pattern.js @@ -0,0 +1,108 @@ +/** + * 05-db-state-pattern.js + * 상태 패턴을 활용한 사전 초기화 큐 + * + * QueuingState와 InitializedState로 분리하여 더 깔끔하게 구현 + */ + +import { EventEmitter } from 'events' + +// 초기화가 필요한 함수 목록 +const METHODS_REQUIRING_CONNECTION = ['query'] + +// 상태 비활성화를 위한 Symbol (이름 충돌 방지) +const deactivate = Symbol('deactivate') + +// 대기 상태: 초기화 전 +class QueuingState { + constructor(db) { + this.db = db + this.commandsQueue = [] + + // 연결이 필요한 함수들을 동적으로 생성 + METHODS_REQUIRING_CONNECTION.forEach(methodName => { + this[methodName] = function (...args) { + console.log('Command queued:', methodName, args[0]) + return new Promise((resolve, reject) => { + const command = () => { + db[methodName](...args) + .then(resolve, reject) + } + this.commandsQueue.push(command) + }) + } + }) + } + + // 상태 비활성화 시 큐 실행 + [deactivate]() { + console.log('Flushing queued commands...') + this.commandsQueue.forEach(command => command()) + this.commandsQueue = [] + } +} + +// 초기화 완료 상태: 비즈니스 로직만 구현 +class InitializedState { + async query(queryString) { + console.log(`Query executed: ${queryString}`) + return { rows: [] } + } +} + +// DB 컨텍스트 클래스 +class DB extends EventEmitter { + constructor() { + super() + this.state = new QueuingState(this) + } + + // 현재 상태로 위임 + async query(queryString) { + return this.state.query(queryString) + } + + connect() { + console.log('Connecting to database...') + setTimeout(() => { + console.log('Database connected!') + this.emit('connected') + + // 상태 전환 + const oldState = this.state + this.state = new InitializedState() + + // 이전 상태 비활성화 (큐 실행) + if (oldState[deactivate]) { + oldState[deactivate]() + } + }, 500) + } +} + +export const db = new DB() + +async function main() { + db.connect() + + // 연결 전 호출 - QueuingState가 처리 + const promise1 = db.query('SELECT * FROM users') + const promise2 = db.query('SELECT * FROM orders') + + const results = await Promise.all([promise1, promise2]) + console.log('All queries completed!') + + // 연결 후 호출 - InitializedState가 처리 + setTimeout(async () => { + await db.query('SELECT * FROM products') + }, 600) +} + +main() + +/** + * 장점: + * - 상태별 로직 분리로 코드 가독성 향상 + * - InitializedState는 순수 비즈니스 로직만 포함 + * - 확장이 용이 (새로운 상태 추가 가능) + */ diff --git a/chapter11/kilhyeonjun/code/06-batch-api.js b/chapter11/kilhyeonjun/code/06-batch-api.js new file mode 100644 index 0000000..3d60913 --- /dev/null +++ b/chapter11/kilhyeonjun/code/06-batch-api.js @@ -0,0 +1,85 @@ +/** + * 06-batch-api.js + * 비동기 요청 일괄 처리 (Batching) + * + * 동일한 요청이 진행 중일 때 새 요청을 시작하지 않고 + * 기존 요청에 편승(piggyback) + */ + +// 느린 API 시뮬레이션 +async function slowApiCall(key) { + console.log(`[API] Starting request for: ${key}`) + await new Promise(resolve => setTimeout(resolve, 1000)) + console.log(`[API] Completed request for: ${key}`) + return { key, data: `Result for ${key}`, timestamp: Date.now() } +} + +// 배치 처리 래퍼 +function createBatchedApi(originalApi) { + const runningRequests = new Map() + + return async function batchedApi(key) { + // 동일한 요청이 진행 중이면 해당 Promise 반환 + if (runningRequests.has(key)) { + console.log(`[Batch] Piggybacking on existing request: ${key}`) + return runningRequests.get(key) + } + + // 새 요청 시작 + console.log(`[Batch] Starting new request: ${key}`) + const resultPromise = originalApi(key) + runningRequests.set(key, resultPromise) + + // 요청 완료 시 Map에서 제거 + resultPromise.finally(() => { + runningRequests.delete(key) + console.log(`[Batch] Request completed and cleared: ${key}`) + }) + + return resultPromise + } +} + +// 배치 처리가 적용된 API +const batchedApiCall = createBatchedApi(slowApiCall) + +async function main() { + console.log('=== Without Batching ===') + // 배치 없이 동일 요청 3번 + const start1 = Date.now() + await Promise.all([ + slowApiCall('user:1'), + slowApiCall('user:1'), + slowApiCall('user:1') + ]) + console.log(`Time without batching: ${Date.now() - start1}ms\n`) + + console.log('=== With Batching ===') + // 배치 적용하여 동일 요청 3번 + const start2 = Date.now() + await Promise.all([ + batchedApiCall('user:1'), + batchedApiCall('user:1'), + batchedApiCall('user:1') + ]) + console.log(`Time with batching: ${Date.now() - start2}ms\n`) + + console.log('=== Different Keys ===') + // 다른 키는 별도 요청 + const start3 = Date.now() + await Promise.all([ + batchedApiCall('user:1'), + batchedApiCall('user:2'), + batchedApiCall('user:1') // user:1과 배치됨 + ]) + console.log(`Time with different keys: ${Date.now() - start3}ms`) +} + +main() + +/** + * 실행 결과: + * - Without Batching: ~3000ms (3번 순차 실행) + * - With Batching: ~1000ms (1번만 실행, 3명이 공유) + * - Different Keys: ~1000ms (2개 병렬 실행) + */ diff --git a/chapter11/kilhyeonjun/code/07-cache-api.js b/chapter11/kilhyeonjun/code/07-cache-api.js new file mode 100644 index 0000000..e53aff7 --- /dev/null +++ b/chapter11/kilhyeonjun/code/07-cache-api.js @@ -0,0 +1,146 @@ +/** + * 07-cache-api.js + * 비동기 요청 캐싱 (Caching) + * + * 배치 처리에 TTL 캐시를 추가하여 결과 재사용 + */ + +// 느린 API 시뮬레이션 +async function slowApiCall(key) { + console.log(`[API] Starting request for: ${key}`) + await new Promise(resolve => setTimeout(resolve, 1000)) + console.log(`[API] Completed request for: ${key}`) + return { key, data: `Result for ${key}`, timestamp: Date.now() } +} + +// 캐시 + 배치 처리 래퍼 +function createCachedApi(originalApi, ttlMs = 5000) { + const runningRequests = new Map() // 진행 중인 요청 + const cache = new Map() // 완료된 결과 캐시 + + return async function cachedApi(key) { + // 1. 캐시 확인 + const cached = cache.get(key) + if (cached) { + const age = Date.now() - cached.timestamp + if (age < ttlMs) { + console.log(`[Cache] HIT for ${key} (age: ${age}ms)`) + return cached.value + } + // TTL 만료 + console.log(`[Cache] EXPIRED for ${key}`) + cache.delete(key) + } + + // 2. 진행 중인 요청 확인 (배치) + if (runningRequests.has(key)) { + console.log(`[Batch] Piggybacking on existing request: ${key}`) + return runningRequests.get(key) + } + + // 3. 새 요청 시작 + console.log(`[Cache] MISS for ${key}, starting new request`) + const resultPromise = originalApi(key) + runningRequests.set(key, resultPromise) + + try { + const result = await resultPromise + // 결과 캐시 + cache.set(key, { + value: result, + timestamp: Date.now() + }) + return result + } finally { + runningRequests.delete(key) + } + } +} + +// 캐시 무효화 기능 추가 버전 +function createCachedApiWithInvalidation(originalApi, ttlMs = 5000) { + const runningRequests = new Map() + const cache = new Map() + + const cachedApi = async function(key) { + const cached = cache.get(key) + if (cached && (Date.now() - cached.timestamp) < ttlMs) { + console.log(`[Cache] HIT for ${key}`) + return cached.value + } + + if (runningRequests.has(key)) { + return runningRequests.get(key) + } + + const resultPromise = originalApi(key) + runningRequests.set(key, resultPromise) + + try { + const result = await resultPromise + cache.set(key, { value: result, timestamp: Date.now() }) + return result + } finally { + runningRequests.delete(key) + } + } + + // 캐시 무효화 메서드 + cachedApi.invalidate = (key) => { + console.log(`[Cache] Invalidating: ${key}`) + cache.delete(key) + } + + cachedApi.invalidateAll = () => { + console.log(`[Cache] Invalidating all entries`) + cache.clear() + } + + cachedApi.getStats = () => ({ + cacheSize: cache.size, + runningRequests: runningRequests.size + }) + + return cachedApi +} + +// 캐시가 적용된 API +const cachedApiCall = createCachedApi(slowApiCall, 3000) + +async function main() { + console.log('=== First Call (Cache MISS) ===') + const start1 = Date.now() + const result1 = await cachedApiCall('user:1') + console.log(`Time: ${Date.now() - start1}ms\n`) + + console.log('=== Second Call (Cache HIT) ===') + const start2 = Date.now() + const result2 = await cachedApiCall('user:1') + console.log(`Time: ${Date.now() - start2}ms\n`) + + console.log('=== Concurrent Calls (Batch + Cache) ===') + const start3 = Date.now() + const results = await Promise.all([ + cachedApiCall('user:2'), + cachedApiCall('user:2'), + cachedApiCall('user:2') + ]) + console.log(`Time: ${Date.now() - start3}ms\n`) + + console.log('=== After TTL Expiry ===') + console.log('Waiting 4 seconds...') + await new Promise(resolve => setTimeout(resolve, 4000)) + const start4 = Date.now() + await cachedApiCall('user:1') + console.log(`Time: ${Date.now() - start4}ms`) +} + +main() + +/** + * 캐싱 고려사항: + * - TTL 설정: 데이터 특성에 맞는 적절한 만료 시간 + * - 메모리 관리: LRU 알고리즘으로 오래된 항목 제거 + * - 무효화 전략: 데이터 변경 시 캐시 동기화 + * - 분산 캐시: 여러 서버 간 캐시 공유 (Redis 등) + */ diff --git a/chapter11/kilhyeonjun/code/08-cancel-basic.js b/chapter11/kilhyeonjun/code/08-cancel-basic.js new file mode 100644 index 0000000..10e4015 --- /dev/null +++ b/chapter11/kilhyeonjun/code/08-cancel-basic.js @@ -0,0 +1,116 @@ +/** + * 08-cancel-basic.js + * 비동기 작업 취소 - 기본 패턴 + * + * cancelRequested 플래그를 사용한 취소 구현 + */ + +// 커스텀 취소 에러 +class CancelError extends Error { + constructor(message = 'Operation cancelled') { + super(message) + this.name = 'CancelError' + } +} + +// 비동기 작업 시뮬레이션 +async function asyncStep(name, durationMs) { + console.log(`[Step] Starting: ${name}`) + await new Promise(resolve => setTimeout(resolve, durationMs)) + console.log(`[Step] Completed: ${name}`) + return `Result of ${name}` +} + +// 취소 가능한 비동기 함수 (기본 패턴) +async function cancellableOperation(cancelObj) { + const results = [] + + // Step 1 + const res1 = await asyncStep('Step 1', 500) + if (cancelObj.cancelRequested) { + throw new CancelError('Cancelled after Step 1') + } + results.push(res1) + + // Step 2 + const res2 = await asyncStep('Step 2', 500) + if (cancelObj.cancelRequested) { + throw new CancelError('Cancelled after Step 2') + } + results.push(res2) + + // Step 3 + const res3 = await asyncStep('Step 3', 500) + if (cancelObj.cancelRequested) { + throw new CancelError('Cancelled after Step 3') + } + results.push(res3) + + return results +} + +// 실행 예제 +async function main() { + console.log('=== Example 1: Complete without cancellation ===') + const cancelObj1 = { cancelRequested: false } + + try { + const results = await cancellableOperation(cancelObj1) + console.log('Results:', results) + } catch (err) { + if (err instanceof CancelError) { + console.log('Operation was cancelled:', err.message) + } else { + throw err + } + } + + console.log('\n=== Example 2: Cancel after 700ms ===') + const cancelObj2 = { cancelRequested: false } + + // 700ms 후 취소 요청 + setTimeout(() => { + console.log('[Main] Requesting cancellation...') + cancelObj2.cancelRequested = true + }, 700) + + try { + const results = await cancellableOperation(cancelObj2) + console.log('Results:', results) + } catch (err) { + if (err instanceof CancelError) { + console.log('Operation was cancelled:', err.message) + } else { + throw err + } + } + + console.log('\n=== Example 3: Cancel before start ===') + const cancelObj3 = { cancelRequested: true } + + try { + const results = await cancellableOperation(cancelObj3) + console.log('Results:', results) + } catch (err) { + if (err instanceof CancelError) { + console.log('Operation was cancelled:', err.message) + } else { + throw err + } + } +} + +main() + +/** + * 기본 패턴의 장단점: + * + * 장점: + * - 간단하고 이해하기 쉬움 + * - 외부 라이브러리 불필요 + * + * 단점: + * - 매 단계마다 수동으로 체크 필요 + * - 코드 중복 발생 + * - 비동기 호출 진행 중에는 취소 불가 + */ diff --git a/chapter11/kilhyeonjun/code/09-cancel-wrapper.js b/chapter11/kilhyeonjun/code/09-cancel-wrapper.js new file mode 100644 index 0000000..d64c507 --- /dev/null +++ b/chapter11/kilhyeonjun/code/09-cancel-wrapper.js @@ -0,0 +1,145 @@ +/** + * 09-cancel-wrapper.js + * 비동기 작업 취소 - 래퍼 패턴 + * + * 비동기 호출을 래핑하여 취소 로직 재사용 + */ + +// 커스텀 취소 에러 +class CancelError extends Error { + constructor(message = 'Operation cancelled') { + super(message) + this.name = 'CancelError' + } +} + +// 취소 가능한 래퍼 생성 팩토리 +function createCancelWrapper() { + let cancelRequested = false + + function cancel() { + cancelRequested = true + } + + // 비동기 함수를 래핑하는 함수 + function cancelWrapper(asyncFn, ...args) { + if (cancelRequested) { + return Promise.reject(new CancelError()) + } + return asyncFn(...args) + } + + return { cancel, cancelWrapper } +} + +// 비동기 작업 시뮬레이션 +async function asyncStep(name, durationMs) { + console.log(`[Step] Starting: ${name}`) + await new Promise(resolve => setTimeout(resolve, durationMs)) + console.log(`[Step] Completed: ${name}`) + return `Result of ${name}` +} + +// 취소 래퍼를 사용한 함수 +async function operationWithWrapper(cancelWrapper) { + const results = [] + + // 각 비동기 호출을 래퍼로 감싸기 + results.push(await cancelWrapper(asyncStep, 'Step 1', 500)) + results.push(await cancelWrapper(asyncStep, 'Step 2', 500)) + results.push(await cancelWrapper(asyncStep, 'Step 3', 500)) + + return results +} + +// HTTP 요청 시뮬레이션 +async function fetchData(url) { + console.log(`[Fetch] Requesting: ${url}`) + await new Promise(resolve => setTimeout(resolve, 300)) + console.log(`[Fetch] Received: ${url}`) + return { url, data: 'response data' } +} + +// 복잡한 작업 예제 +async function complexOperation(cancelWrapper) { + // 순차 요청 + const user = await cancelWrapper(fetchData, '/api/user') + const orders = await cancelWrapper(fetchData, `/api/orders?user=${user.url}`) + + // 병렬 요청 + const [products, reviews] = await Promise.all([ + cancelWrapper(fetchData, '/api/products'), + cancelWrapper(fetchData, '/api/reviews') + ]) + + return { user, orders, products, reviews } +} + +async function main() { + console.log('=== Example 1: Complete without cancellation ===') + const { cancel: cancel1, cancelWrapper: wrapper1 } = createCancelWrapper() + + try { + const results = await operationWithWrapper(wrapper1) + console.log('Results:', results) + } catch (err) { + if (err instanceof CancelError) { + console.log('Cancelled:', err.message) + } else { + throw err + } + } + + console.log('\n=== Example 2: Cancel during operation ===') + const { cancel: cancel2, cancelWrapper: wrapper2 } = createCancelWrapper() + + setTimeout(() => { + console.log('[Main] Cancelling...') + cancel2() + }, 700) + + try { + const results = await operationWithWrapper(wrapper2) + console.log('Results:', results) + } catch (err) { + if (err instanceof CancelError) { + console.log('Cancelled:', err.message) + } else { + throw err + } + } + + console.log('\n=== Example 3: Complex operation with cancellation ===') + const { cancel: cancel3, cancelWrapper: wrapper3 } = createCancelWrapper() + + setTimeout(() => { + console.log('[Main] Cancelling complex operation...') + cancel3() + }, 500) + + try { + const results = await complexOperation(wrapper3) + console.log('Results:', results) + } catch (err) { + if (err instanceof CancelError) { + console.log('Cancelled:', err.message) + } else { + throw err + } + } +} + +main() + +/** + * 래퍼 패턴의 장단점: + * + * 장점: + * - 취소 로직 재사용 + * - 코드가 더 깔끔함 + * - 기존 함수 수정 없이 적용 가능 + * + * 단점: + * - 모든 비동기 호출을 래핑해야 함 + * - 래핑을 잊으면 취소가 안됨 + */ diff --git a/chapter11/kilhyeonjun/code/10-cancel-generator.js b/chapter11/kilhyeonjun/code/10-cancel-generator.js new file mode 100644 index 0000000..f39aaca --- /dev/null +++ b/chapter11/kilhyeonjun/code/10-cancel-generator.js @@ -0,0 +1,178 @@ +/** + * 10-cancel-generator.js + * 비동기 작업 취소 - 제너레이터 패턴 + * + * 제너레이터를 사용하여 자동으로 취소 포인트 생성 + */ + +// 커스텀 취소 에러 +class CancelError extends Error { + constructor(message = 'Operation cancelled') { + super(message) + this.name = 'CancelError' + } +} + +// 제너레이터 기반 취소 가능한 비동기 함수 생성 +function createAsyncCancelable(generatorFn) { + return function(...args) { + const generator = generatorFn(...args) + let cancelRequested = false + + function cancel() { + cancelRequested = true + } + + const promise = new Promise((resolve, reject) => { + async function step(nextValue) { + // 취소 요청 확인 + if (cancelRequested) { + return reject(new CancelError()) + } + + let result + try { + result = generator.next(nextValue) + } catch (err) { + return reject(err) + } + + if (result.done) { + return resolve(result.value) + } + + try { + // Promise 대기 후 다음 단계 + const value = await result.value + step(value) + } catch (err) { + try { + // 에러를 제너레이터에 전달 + result = generator.throw(err) + if (result.done) { + return resolve(result.value) + } + step(result.value) + } catch (thrownErr) { + reject(thrownErr) + } + } + } + + step() + }) + + return { promise, cancel } + } +} + +// 비동기 작업 시뮬레이션 +async function asyncStep(name, durationMs) { + console.log(`[Step] Starting: ${name}`) + await new Promise(resolve => setTimeout(resolve, durationMs)) + console.log(`[Step] Completed: ${name}`) + return `Result of ${name}` +} + +// 제너레이터 함수로 비동기 로직 정의 +const cancellableOperation = createAsyncCancelable(function* () { + const results = [] + + // yield로 비동기 작업 실행 - 각 yield가 취소 포인트 + results.push(yield asyncStep('Step 1', 500)) + results.push(yield asyncStep('Step 2', 500)) + results.push(yield asyncStep('Step 3', 500)) + + return results +}) + +// 더 복잡한 예제 +const complexOperation = createAsyncCancelable(function* () { + console.log('[Complex] Starting complex operation') + + const user = yield asyncStep('Fetch User', 300) + console.log('[Complex] Got user:', user) + + const orders = yield asyncStep('Fetch Orders', 300) + console.log('[Complex] Got orders:', orders) + + // 조건부 로직도 가능 + if (orders) { + const details = yield asyncStep('Fetch Details', 300) + console.log('[Complex] Got details:', details) + } + + return { user, orders } +}) + +async function main() { + console.log('=== Example 1: Complete without cancellation ===') + const { promise: promise1, cancel: cancel1 } = cancellableOperation() + + try { + const results = await promise1 + console.log('Results:', results) + } catch (err) { + if (err instanceof CancelError) { + console.log('Cancelled:', err.message) + } else { + throw err + } + } + + console.log('\n=== Example 2: Cancel during operation ===') + const { promise: promise2, cancel: cancel2 } = cancellableOperation() + + setTimeout(() => { + console.log('[Main] Cancelling...') + cancel2() + }, 700) + + try { + const results = await promise2 + console.log('Results:', results) + } catch (err) { + if (err instanceof CancelError) { + console.log('Cancelled:', err.message) + } else { + throw err + } + } + + console.log('\n=== Example 3: Complex operation ===') + const { promise: promise3, cancel: cancel3 } = complexOperation() + + setTimeout(() => { + console.log('[Main] Cancelling complex operation...') + cancel3() + }, 500) + + try { + const results = await promise3 + console.log('Results:', results) + } catch (err) { + if (err instanceof CancelError) { + console.log('Cancelled:', err.message) + } else { + throw err + } + } +} + +main() + +/** + * 제너레이터 패턴의 장단점: + * + * 장점: + * - 모든 yield가 자동으로 취소 포인트 + * - 비즈니스 로직에 취소 코드 없음 + * - 가독성이 좋음 + * + * 단점: + * - 제너레이터 문법 이해 필요 + * - 약간의 성능 오버헤드 + * + * 참고: caf 라이브러리가 이 패턴을 구현 + * https://github.com/getify/CAF + */ diff --git a/chapter11/kilhyeonjun/code/11-subset-sum-blocking.js b/chapter11/kilhyeonjun/code/11-subset-sum-blocking.js new file mode 100644 index 0000000..87c0eb9 --- /dev/null +++ b/chapter11/kilhyeonjun/code/11-subset-sum-blocking.js @@ -0,0 +1,109 @@ +/** + * 11-subset-sum-blocking.js + * CPU 바운드 작업 - 이벤트 루프 차단 문제 + * + * 부분집합 합계 문제 (Subset Sum Problem) + * 주어진 집합에서 합이 특정 값이 되는 부분집합 찾기 + * 시간 복잡도: O(2^n) - NP-완전 문제 + */ + +// 부분집합 합계 문제 - 동기 버전 (이벤트 루프 차단) +class SubsetSumSync { + constructor(set, sum) { + this.set = set + this.sum = sum + this.results = [] + } + + // 재귀적으로 모든 조합 탐색 + _combine(set, subset) { + for (let i = 0; i < set.length; i++) { + const newSubset = subset.concat(set[i]) + const currentSum = newSubset.reduce((a, b) => a + b, 0) + + if (currentSum === this.sum) { + this.results.push(newSubset) + } + + // 남은 원소들로 계속 탐색 + this._combine(set.slice(i + 1), newSubset) + } + } + + run() { + this._combine(this.set, []) + return this.results + } +} + +// 이벤트 루프 차단 시연 +async function demonstrateBlocking() { + console.log('=== Event Loop Blocking Demo ===\n') + + // 작은 집합 테스트 + console.log('Small set (10 elements):') + const smallSet = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + const smallSum = 15 + + const start1 = Date.now() + const solver1 = new SubsetSumSync(smallSet, smallSum) + const results1 = solver1.run() + console.log(`Found ${results1.length} solutions in ${Date.now() - start1}ms`) + console.log('Sample solutions:', results1.slice(0, 3)) + + // 중간 크기 집합 테스트 + console.log('\nMedium set (20 elements):') + const mediumSet = Array.from({ length: 20 }, (_, i) => i + 1) + const mediumSum = 50 + + const start2 = Date.now() + const solver2 = new SubsetSumSync(mediumSet, mediumSum) + const results2 = solver2.run() + console.log(`Found ${results2.length} solutions in ${Date.now() - start2}ms`) + + // 이벤트 루프 차단 시연 + console.log('\n=== Demonstrating Event Loop Blocking ===') + console.log('Starting computation...') + + // setInterval로 이벤트 루프 모니터링 + let ticks = 0 + const tickInterval = setInterval(() => { + ticks++ + console.log(`[Tick ${ticks}] Event loop is running`) + }, 100) + + // 큰 집합 계산 (이벤트 루프 차단) + setTimeout(() => { + console.log('\n[Compute] Starting heavy computation...') + const heavySet = Array.from({ length: 22 }, (_, i) => i + 1) + const heavySum = 100 + + const start = Date.now() + const solver = new SubsetSumSync(heavySet, heavySum) + const results = solver.run() + console.log(`[Compute] Done! Found ${results.length} solutions in ${Date.now() - start}ms`) + console.log('[Compute] Notice how tick messages stopped during computation!') + + clearInterval(tickInterval) + }, 500) + + // 2초 후 정리 + setTimeout(() => { + console.log('\n[End] Demo complete') + }, 10000) +} + +demonstrateBlocking() + +/** + * 문제점: + * - 동기 계산 중 이벤트 루프가 완전히 차단됨 + * - 타이머, I/O 콜백이 실행되지 않음 + * - HTTP 요청 처리 불가 + * - 서버가 응답하지 않는 것처럼 보임 + * + * 해결책: + * - setImmediate 인터리빙 (12-subset-sum-interleaving.js) + * - 외부 프로세스 (13-process-pool.js) + * - 작업자 스레드 (14-thread-pool.js) + */ diff --git a/chapter11/kilhyeonjun/code/12-subset-sum-interleaving.js b/chapter11/kilhyeonjun/code/12-subset-sum-interleaving.js new file mode 100644 index 0000000..32bfffa --- /dev/null +++ b/chapter11/kilhyeonjun/code/12-subset-sum-interleaving.js @@ -0,0 +1,161 @@ +/** + * 12-subset-sum-interleaving.js + * CPU 바운드 작업 - setImmediate 인터리빙 + * + * setImmediate()를 사용하여 계산 단계 사이에 + * 다른 I/O 작업이 실행될 수 있게 함 + */ + +import { EventEmitter } from 'events' + +// 부분집합 합계 문제 - 인터리빙 버전 +class SubsetSumInterleaved extends EventEmitter { + constructor(set, sum) { + super() + this.set = set + this.sum = sum + this.results = [] + this.iterations = 0 + } + + _combineInterleaved(set, subset, callback) { + this.iterations++ + + // 주기적으로 진행 상황 이벤트 발생 + if (this.iterations % 10000 === 0) { + this.emit('progress', { + iterations: this.iterations, + results: this.results.length + }) + } + + // setImmediate를 사용하여 다른 작업에 양보 + setImmediate(() => { + if (set.length === 0) { + return callback() + } + + // 현재 원소를 포함하는 경우 처리 + const currentElement = set[0] + const remaining = set.slice(1) + const newSubset = subset.concat(currentElement) + const currentSum = newSubset.reduce((a, b) => a + b, 0) + + if (currentSum === this.sum) { + this.results.push(newSubset) + this.emit('match', newSubset) + } + + // 현재 원소를 포함하는 분기 + this._combineInterleaved(remaining, newSubset, () => { + // 현재 원소를 제외하는 분기 + this._combineInterleaved(remaining, subset, callback) + }) + }) + } + + run(callback) { + const startTime = Date.now() + this._combineInterleaved(this.set, [], () => { + const duration = Date.now() - startTime + callback(null, { + results: this.results, + iterations: this.iterations, + duration + }) + }) + } + + // Promise 버전 + runAsync() { + return new Promise((resolve, reject) => { + this.run((err, result) => { + if (err) reject(err) + else resolve(result) + }) + }) + } +} + +// 이벤트 루프가 차단되지 않음을 시연 +async function demonstrateInterleaving() { + console.log('=== setImmediate Interleaving Demo ===\n') + + const set = Array.from({ length: 18 }, (_, i) => i + 1) + const sum = 50 + + console.log(`Finding subsets of [1..${set.length}] that sum to ${sum}`) + console.log('Notice how tick messages continue during computation!\n') + + // 이벤트 루프 모니터링 + let ticks = 0 + const tickInterval = setInterval(() => { + ticks++ + console.log(`[Tick ${ticks}] Event loop is responsive`) + }, 200) + + // 인터리빙 계산 실행 + const solver = new SubsetSumInterleaved(set, sum) + + // 진행 상황 이벤트 리스너 + solver.on('progress', ({ iterations, results }) => { + console.log(`[Progress] Iterations: ${iterations}, Found: ${results} solutions`) + }) + + // 결과 찾을 때마다 이벤트 + let matchCount = 0 + solver.on('match', (subset) => { + matchCount++ + if (matchCount <= 3) { + console.log(`[Match #${matchCount}] ${JSON.stringify(subset)}`) + } + }) + + try { + const result = await solver.runAsync() + console.log(`\n=== Computation Complete ===`) + console.log(`Total solutions: ${result.results.length}`) + console.log(`Total iterations: ${result.iterations}`) + console.log(`Duration: ${result.duration}ms`) + console.log(`Ticks during computation: ${ticks}`) + } finally { + clearInterval(tickInterval) + } +} + +// 비교: 동기 vs 인터리빙 +async function comparePerformance() { + console.log('\n=== Performance Comparison ===\n') + + const set = Array.from({ length: 15 }, (_, i) => i + 1) + const sum = 30 + + // 인터리빙 버전 + console.log('Interleaved version:') + const solver = new SubsetSumInterleaved(set, sum) + const result = await solver.runAsync() + console.log(`Found ${result.results.length} solutions in ${result.duration}ms`) + + console.log('\nNote: Interleaving has overhead but keeps event loop responsive') +} + +demonstrateInterleaving().then(comparePerformance) + +/** + * setImmediate 인터리빙의 장단점: + * + * 장점: + * - 이벤트 루프가 차단되지 않음 + * - 추가 프로세스/스레드 불필요 + * - 메모리 공유 가능 + * + * 단점: + * - 성능 오버헤드 (컨텍스트 스위칭) + * - 단계가 길면 여전히 지연 발생 + * - 단일 코어만 사용 + * + * 적합한 경우: + * - 빠른 단계로 분할 가능한 작업 + * - 이벤트 루프 응답성이 중요한 경우 + * - 리소스가 제한된 환경 + */ diff --git a/chapter11/kilhyeonjun/code/13-process-pool.js b/chapter11/kilhyeonjun/code/13-process-pool.js new file mode 100644 index 0000000..41da3fb --- /dev/null +++ b/chapter11/kilhyeonjun/code/13-process-pool.js @@ -0,0 +1,178 @@ +/** + * 13-process-pool.js + * CPU 바운드 작업 - 외부 프로세스 풀 + * + * child_process.fork()를 사용하여 작업을 + * 별도의 프로세스에서 실행 + */ + +import { fork } from 'child_process' +import { EventEmitter } from 'events' +import { dirname, join } from 'path' +import { fileURLToPath } from 'url' + +const __dirname = dirname(fileURLToPath(import.meta.url)) + +// 프로세스 풀 클래스 +class ProcessPool extends EventEmitter { + constructor(workerScript, poolSize) { + super() + this.workerScript = workerScript + this.poolSize = poolSize + this.workers = [] + this.freeWorkers = [] + this.queue = [] + + // 워커 프로세스 생성 + for (let i = 0; i < poolSize; i++) { + this._createWorker() + } + } + + _createWorker() { + const worker = fork(this.workerScript) + worker.id = this.workers.length + + worker.on('message', (msg) => { + if (msg.type === 'result') { + const { resolve, reject } = worker.currentTask + worker.currentTask = null + + if (msg.error) { + reject(new Error(msg.error)) + } else { + resolve(msg.data) + } + + // 워커를 다시 사용 가능 상태로 + this.freeWorkers.push(worker) + this._processQueue() + } + }) + + worker.on('error', (err) => { + console.error(`[Worker ${worker.id}] Error:`, err) + if (worker.currentTask) { + worker.currentTask.reject(err) + } + }) + + worker.on('exit', (code) => { + console.log(`[Worker ${worker.id}] Exited with code ${code}`) + // 필요시 워커 재생성 로직 추가 + }) + + this.workers.push(worker) + this.freeWorkers.push(worker) + console.log(`[Pool] Created worker ${worker.id}`) + } + + _processQueue() { + if (this.queue.length === 0 || this.freeWorkers.length === 0) { + return + } + + const task = this.queue.shift() + const worker = this.freeWorkers.shift() + + worker.currentTask = task + worker.send({ type: 'task', data: task.data }) + } + + // 작업 실행 + run(taskData) { + return new Promise((resolve, reject) => { + const task = { data: taskData, resolve, reject } + this.queue.push(task) + this._processQueue() + }) + } + + // 풀 종료 + async shutdown() { + console.log('[Pool] Shutting down...') + for (const worker of this.workers) { + worker.send({ type: 'shutdown' }) + } + // 모든 워커가 종료될 때까지 대기 + await new Promise(resolve => setTimeout(resolve, 100)) + console.log('[Pool] Shutdown complete') + } + + // 상태 정보 + getStats() { + return { + totalWorkers: this.workers.length, + freeWorkers: this.freeWorkers.length, + queuedTasks: this.queue.length + } + } +} + +// 메인 실행 +async function main() { + console.log('=== Process Pool Demo ===\n') + + // 워커 스크립트 경로 + const workerScript = join(__dirname, '13-process-worker.js') + + // 풀 생성 (CPU 코어 수에 맞춰 조정) + const pool = new ProcessPool(workerScript, 4) + console.log('') + + // 이벤트 루프 모니터링 + let ticks = 0 + const tickInterval = setInterval(() => { + ticks++ + const stats = pool.getStats() + console.log(`[Tick ${ticks}] Event loop responsive | Workers: ${stats.freeWorkers}/${stats.totalWorkers} free`) + }, 200) + + try { + // 여러 작업 병렬 실행 + console.log('\n=== Running Multiple Tasks ===') + const tasks = [ + { set: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], sum: 30 }, + { set: [2, 4, 6, 8, 10, 12, 14, 16, 18, 20], sum: 40 }, + { set: [1, 3, 5, 7, 9, 11, 13, 15, 17, 19], sum: 35 }, + { set: [5, 10, 15, 20, 25, 30, 35, 40, 45, 50], sum: 100 } + ] + + const startTime = Date.now() + const results = await Promise.all( + tasks.map(task => pool.run(task)) + ) + + console.log(`\n=== Results ===`) + results.forEach((result, i) => { + console.log(`Task ${i + 1}: Found ${result.count} solutions in ${result.duration}ms`) + }) + console.log(`Total time: ${Date.now() - startTime}ms (parallel execution)`) + console.log(`Event loop ticks during execution: ${ticks}`) + + } finally { + clearInterval(tickInterval) + await pool.shutdown() + } +} + +main().catch(console.error) + +/** + * 외부 프로세스의 장단점: + * + * 장점: + * - 이벤트 루프 완전 분리 + * - 멀티코어 활용 + * - 크래시 격리 (워커 크래시가 메인 프로세스에 영향 없음) + * + * 단점: + * - 프로세스 생성 비용 + * - IPC 오버헤드 + * - 메모리 사용량 증가 + * + * 적합한 경우: + * - 장시간 실행되는 CPU 집약적 작업 + * - 멀티코어 활용이 필요한 경우 + * - 격리가 필요한 신뢰할 수 없는 코드 실행 + */ diff --git a/chapter11/kilhyeonjun/code/13-process-worker.js b/chapter11/kilhyeonjun/code/13-process-worker.js new file mode 100644 index 0000000..425708c --- /dev/null +++ b/chapter11/kilhyeonjun/code/13-process-worker.js @@ -0,0 +1,58 @@ +/** + * 13-process-worker.js + * 프로세스 풀의 워커 스크립트 + * + * 부분집합 합계 문제를 계산하는 워커 + */ + +// 부분집합 합계 계산 +function subsetSum(set, sum) { + const results = [] + + function combine(remaining, subset) { + for (let i = 0; i < remaining.length; i++) { + const newSubset = subset.concat(remaining[i]) + const currentSum = newSubset.reduce((a, b) => a + b, 0) + + if (currentSum === sum) { + results.push(newSubset) + } + + combine(remaining.slice(i + 1), newSubset) + } + } + + combine(set, []) + return results +} + +// 메시지 처리 +process.on('message', (msg) => { + if (msg.type === 'task') { + const { set, sum } = msg.data + const startTime = Date.now() + + try { + const results = subsetSum(set, sum) + const duration = Date.now() - startTime + + process.send({ + type: 'result', + data: { + count: results.length, + duration, + sample: results.slice(0, 3) + } + }) + } catch (error) { + process.send({ + type: 'result', + error: error.message + }) + } + } else if (msg.type === 'shutdown') { + process.exit(0) + } +}) + +console.log(`[Worker ${process.pid}] Ready`) diff --git a/chapter11/kilhyeonjun/code/14-thread-pool.js b/chapter11/kilhyeonjun/code/14-thread-pool.js new file mode 100644 index 0000000..41e7204 --- /dev/null +++ b/chapter11/kilhyeonjun/code/14-thread-pool.js @@ -0,0 +1,235 @@ +/** + * 14-thread-pool.js + * CPU 바운드 작업 - 작업자 스레드 풀 + * + * worker_threads를 사용하여 작업을 + * 별도의 스레드에서 실행 + */ + +import { Worker, isMainThread, parentPort, workerData } from 'worker_threads' +import { EventEmitter } from 'events' +import { fileURLToPath } from 'url' + +// 워커 스레드 코드 (같은 파일에서 실행) +if (!isMainThread) { + // 부분집합 합계 계산 + function subsetSum(set, sum) { + const results = [] + + function combine(remaining, subset) { + for (let i = 0; i < remaining.length; i++) { + const newSubset = subset.concat(remaining[i]) + const currentSum = newSubset.reduce((a, b) => a + b, 0) + + if (currentSum === sum) { + results.push(newSubset) + } + + combine(remaining.slice(i + 1), newSubset) + } + } + + combine(set, []) + return results + } + + // 메시지 처리 + parentPort.on('message', (msg) => { + if (msg.type === 'task') { + const { set, sum } = msg.data + const startTime = Date.now() + + try { + const results = subsetSum(set, sum) + const duration = Date.now() - startTime + + parentPort.postMessage({ + type: 'result', + data: { + count: results.length, + duration, + sample: results.slice(0, 3) + } + }) + } catch (error) { + parentPort.postMessage({ + type: 'result', + error: error.message + }) + } + } + }) +} else { + // 메인 스레드 코드 + + // 스레드 풀 클래스 + class ThreadPool extends EventEmitter { + constructor(poolSize) { + super() + this.poolSize = poolSize + this.workers = [] + this.freeWorkers = [] + this.queue = [] + + // 워커 스레드 생성 + for (let i = 0; i < poolSize; i++) { + this._createWorker() + } + } + + _createWorker() { + const worker = new Worker(fileURLToPath(import.meta.url)) + worker.id = this.workers.length + + worker.on('message', (msg) => { + if (msg.type === 'result') { + const { resolve, reject } = worker.currentTask + worker.currentTask = null + + if (msg.error) { + reject(new Error(msg.error)) + } else { + resolve(msg.data) + } + + // 워커를 다시 사용 가능 상태로 + this.freeWorkers.push(worker) + this._processQueue() + } + }) + + worker.on('error', (err) => { + console.error(`[Thread ${worker.id}] Error:`, err) + if (worker.currentTask) { + worker.currentTask.reject(err) + } + }) + + worker.on('exit', (code) => { + if (code !== 0) { + console.error(`[Thread ${worker.id}] Exited with code ${code}`) + } + }) + + this.workers.push(worker) + this.freeWorkers.push(worker) + console.log(`[Pool] Created thread ${worker.id}`) + } + + _processQueue() { + if (this.queue.length === 0 || this.freeWorkers.length === 0) { + return + } + + const task = this.queue.shift() + const worker = this.freeWorkers.shift() + + worker.currentTask = task + worker.postMessage({ type: 'task', data: task.data }) + } + + // 작업 실행 + run(taskData) { + return new Promise((resolve, reject) => { + const task = { data: taskData, resolve, reject } + this.queue.push(task) + this._processQueue() + }) + } + + // 풀 종료 + async shutdown() { + console.log('[Pool] Shutting down threads...') + await Promise.all( + this.workers.map(worker => worker.terminate()) + ) + console.log('[Pool] Shutdown complete') + } + + // 상태 정보 + getStats() { + return { + totalWorkers: this.workers.length, + freeWorkers: this.freeWorkers.length, + queuedTasks: this.queue.length + } + } + } + + // 메인 실행 + async function main() { + console.log('=== Thread Pool Demo ===\n') + + // 풀 생성 + const pool = new ThreadPool(4) + console.log('') + + // 이벤트 루프 모니터링 + let ticks = 0 + const tickInterval = setInterval(() => { + ticks++ + const stats = pool.getStats() + console.log(`[Tick ${ticks}] Event loop responsive | Threads: ${stats.freeWorkers}/${stats.totalWorkers} free`) + }, 200) + + try { + // 여러 작업 병렬 실행 + console.log('\n=== Running Multiple Tasks ===') + const tasks = [ + { set: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], sum: 30 }, + { set: [2, 4, 6, 8, 10, 12, 14, 16, 18, 20], sum: 40 }, + { set: [1, 3, 5, 7, 9, 11, 13, 15, 17, 19], sum: 35 }, + { set: [5, 10, 15, 20, 25, 30, 35, 40, 45, 50], sum: 100 } + ] + + const startTime = Date.now() + const results = await Promise.all( + tasks.map(task => pool.run(task)) + ) + + console.log(`\n=== Results ===`) + results.forEach((result, i) => { + console.log(`Task ${i + 1}: Found ${result.count} solutions in ${result.duration}ms`) + }) + console.log(`Total time: ${Date.now() - startTime}ms (parallel execution)`) + console.log(`Event loop ticks during execution: ${ticks}`) + + // 프로세스 풀과 비교 + console.log('\n=== Thread vs Process Comparison ===') + console.log('Threads are lighter weight than processes:') + console.log('- Faster startup time') + console.log('- Lower memory overhead') + console.log('- Can share memory via SharedArrayBuffer') + + } finally { + clearInterval(tickInterval) + await pool.shutdown() + } + } + + main().catch(console.error) +} + +/** + * 작업자 스레드의 장단점: + * + * 장점: + * - 프로세스보다 가벼움 + * - 빠른 시작 시간 + * - SharedArrayBuffer로 메모리 공유 가능 + * - 이벤트 루프 완전 분리 + * + * 단점: + * - Node.js 10.5+ 필요 + * - 복잡한 에러 처리 + * - 공유 메모리 사용 시 동기화 필요 + * + * 권장 라이브러리: + * - workerpool: https://github.com/josdejong/workerpool + * - piscina: https://github.com/piscinajs/piscina + * + * 적합한 경우: + * - 빈번한 CPU 집약적 작업 + * - 메모리 공유가 필요한 경우 + * - 낮은 레이턴시가 중요한 경우 + */ diff --git a/chapter11/kilhyeonjun/code/README.md b/chapter11/kilhyeonjun/code/README.md new file mode 100644 index 0000000..ed6a60c --- /dev/null +++ b/chapter11/kilhyeonjun/code/README.md @@ -0,0 +1,113 @@ +# Chapter 11 코드 예제 + +Chapter 11에서 다루는 고급 레시피의 코드 예제입니다. + +## 실행 방법 + +```bash +# 개별 파일 실행 +node --experimental-vm-modules 01-db-without-queue.js + +# ES 모듈 사용 파일은 package.json에 "type": "module" 필요 +``` + +## 파일 목록 + +### 1. 비동기 초기화 (01-05) + +| 파일 | 설명 | +|------|------| +| `01-db-without-queue.js` | 문제 상황: 초기화 전 쿼리 시 에러 발생 | +| `02-db-local-init.js` | 해결책 1: 매번 연결 상태 확인 | +| `03-db-delayed-startup.js` | 해결책 2: 모든 초기화 완료 후 실행 | +| `04-db-preinitialization-queue.js` | 해결책 3: 사전 초기화 큐 (권장) | +| `05-db-state-pattern.js` | 상태 패턴으로 구조화 | + +### 2. 요청 배치 및 캐싱 (06-07) + +| 파일 | 설명 | +|------|------| +| `06-batch-api.js` | 동일 요청 배치 처리 (피기백) | +| `07-cache-api.js` | TTL 기반 캐싱 + 배치 | + +### 3. 비동기 작업 취소 (08-10) + +| 파일 | 설명 | +|------|------| +| `08-cancel-basic.js` | 기본 패턴: cancelRequested 플래그 | +| `09-cancel-wrapper.js` | 래퍼 패턴: createCancelWrapper | +| `10-cancel-generator.js` | 제너레이터 패턴: createAsyncCancelable | + +### 4. CPU 바운드 작업 (11-14) + +| 파일 | 설명 | +|------|------| +| `11-subset-sum-blocking.js` | 문제 상황: 이벤트 루프 차단 | +| `12-subset-sum-interleaving.js` | 해결책 1: setImmediate 인터리빙 | +| `13-process-pool.js` | 해결책 2: 외부 프로세스 풀 | +| `13-process-worker.js` | 프로세스 풀 워커 스크립트 | +| `14-thread-pool.js` | 해결책 3: 작업자 스레드 풀 | + +## 핵심 코드 스니펫 + +### 사전 초기화 큐 + +```javascript +// 04-db-preinitialization-queue.js +async query(queryString) { + if (!this.connected) { + return new Promise((resolve, reject) => { + this.commandsQueue.push(() => { + this.query(queryString).then(resolve, reject) + }) + }) + } + // 실제 쿼리 실행 +} +``` + +### 요청 배치 + +```javascript +// 06-batch-api.js +if (runningRequests.has(key)) { + return runningRequests.get(key) // 피기백 +} +const promise = originalApi(key) +runningRequests.set(key, promise) +promise.finally(() => runningRequests.delete(key)) +``` + +### 제너레이터 기반 취소 + +```javascript +// 10-cancel-generator.js +const cancellable = createAsyncCancelable(function* () { + const a = yield asyncStep('A') // 취소 포인트 + const b = yield asyncStep('B') // 취소 포인트 + return [a, b] +}) +const { promise, cancel } = cancellable() +``` + +### 스레드 풀 + +```javascript +// 14-thread-pool.js +const pool = new ThreadPool(4) +const results = await Promise.all([ + pool.run({ set: [1,2,3], sum: 5 }), + pool.run({ set: [4,5,6], sum: 10 }) +]) +``` + +## 연습문제 + +`exercises/` 디렉토리에서 연습문제 풀이를 확인할 수 있습니다. + +| 파일 | 연습문제 | +|------|----------| +| `11.1-proxy-queue.js` | Proxy를 사용한 대기열 구현 | +| `11.2-callback-batch-cache.js` | 콜백 기반 배치 및 캐싱 | +| `11.3-deep-cancelable.js` | Deep 취소 가능한 비동기 함수 | +| `11.4-computing-farm.js` | 컴퓨팅 팜 (HTTP + eval/vm) | diff --git a/chapter11/kilhyeonjun/code/exercises/11.1-proxy-queue.js b/chapter11/kilhyeonjun/code/exercises/11.1-proxy-queue.js new file mode 100644 index 0000000..3cf38f8 --- /dev/null +++ b/chapter11/kilhyeonjun/code/exercises/11.1-proxy-queue.js @@ -0,0 +1,178 @@ +/** + * 11.1-proxy-queue.js + * 연습문제 11.1: Proxy를 사용한 대기열 구현 + * + * Proxy를 사용하여 비동기로 초기화되는 모든 컴포넌트에 + * 대기열을 투명하게 적용하는 일반 래퍼 + */ + +import { EventEmitter, once } from 'events' + +/** + * 프록시 기반 초기화 대기열 래퍼 + * + * @param {Object} target - 래핑할 객체 + * @param {Function} initFn - 초기화 함수 (Promise 반환) + * @returns {Proxy} 대기열이 적용된 프록시 객체 + */ +function createQueuedProxy(target, initFn) { + let initialized = false + let initPromise = null + const queue = [] + + // 초기화 시작 + initPromise = initFn().then(() => { + initialized = true + // 대기 중인 호출 실행 + queue.forEach(({ method, args, resolve, reject }) => { + Promise.resolve(target[method](...args)) + .then(resolve) + .catch(reject) + }) + queue.length = 0 + }) + + return new Proxy(target, { + get(obj, prop) { + const value = obj[prop] + + // 함수가 아니면 그대로 반환 + if (typeof value !== 'function') { + return value + } + + // 초기화 완료 후에는 원본 함수 반환 + if (initialized) { + return value.bind(obj) + } + + // 초기화 전에는 큐에 저장하는 래퍼 반환 + return function(...args) { + return new Promise((resolve, reject) => { + queue.push({ method: prop, args, resolve, reject }) + }) + } + } + }) +} + +// 예제: 비동기 초기화가 필요한 DB 클래스 +class Database extends EventEmitter { + constructor() { + super() + this.connected = false + this.data = new Map() + } + + async connect() { + console.log('[DB] Connecting...') + await new Promise(resolve => setTimeout(resolve, 500)) + this.connected = true + console.log('[DB] Connected!') + this.emit('connected') + } + + async query(sql) { + if (!this.connected) { + throw new Error('Not connected') + } + console.log(`[DB] Executing: ${sql}`) + await new Promise(resolve => setTimeout(resolve, 100)) + return { rows: [], sql } + } + + async insert(table, data) { + if (!this.connected) { + throw new Error('Not connected') + } + console.log(`[DB] Inserting into ${table}:`, data) + const id = Date.now() + this.data.set(id, { table, ...data }) + return { id } + } +} + +// 예제: 비동기 초기화가 필요한 캐시 클래스 +class Cache extends EventEmitter { + constructor() { + super() + this.ready = false + this.store = new Map() + } + + async initialize() { + console.log('[Cache] Initializing...') + await new Promise(resolve => setTimeout(resolve, 300)) + this.ready = true + console.log('[Cache] Ready!') + this.emit('ready') + } + + async get(key) { + if (!this.ready) throw new Error('Not ready') + console.log(`[Cache] Getting: ${key}`) + return this.store.get(key) + } + + async set(key, value, ttl = 60000) { + if (!this.ready) throw new Error('Not ready') + console.log(`[Cache] Setting: ${key} = ${value}`) + this.store.set(key, value) + setTimeout(() => this.store.delete(key), ttl) + return true + } +} + +// 사용 예제 +async function main() { + console.log('=== Proxy Queue Demo ===\n') + + // 1. Database with proxy queue + console.log('--- Database Example ---') + const rawDb = new Database() + const db = createQueuedProxy(rawDb, () => rawDb.connect()) + + // 초기화 전에 호출 - 자동으로 큐에 저장됨 + const promise1 = db.query('SELECT * FROM users') + const promise2 = db.insert('users', { name: 'Alice' }) + const promise3 = db.query('SELECT * FROM orders') + + console.log('[Main] All calls queued, waiting for results...') + const results = await Promise.all([promise1, promise2, promise3]) + console.log('[Main] Results:', results) + + // 초기화 후 호출 - 바로 실행 + console.log('\n[Main] Now calling after initialization:') + const result = await db.query('SELECT * FROM products') + console.log('[Main] Immediate result:', result) + + // 2. Cache with proxy queue + console.log('\n--- Cache Example ---') + const rawCache = new Cache() + const cache = createQueuedProxy(rawCache, () => rawCache.initialize()) + + // 초기화 전에 호출 + const setPromise = cache.set('user:1', { name: 'Bob' }) + const getPromise = cache.get('user:1') + + console.log('[Main] Cache calls queued...') + await setPromise + const cachedValue = await getPromise + console.log('[Main] Cached value:', cachedValue) +} + +main().catch(console.error) + +/** + * Proxy 기반 대기열의 장점: + * + * 1. 투명성: 사용자 코드에서 초기화 상태를 전혀 신경 쓰지 않아도 됨 + * 2. 일반성: 어떤 객체에도 적용 가능 + * 3. 자동화: 모든 메서드 호출을 자동으로 처리 + * + * 확장 가능한 기능: + * - 특정 메서드만 대기열 적용 + * - 재초기화 지원 + * - 타임아웃 처리 + * - 에러 핸들링 개선 + */ diff --git a/chapter11/kilhyeonjun/code/exercises/11.2-callback-batch-cache.js b/chapter11/kilhyeonjun/code/exercises/11.2-callback-batch-cache.js new file mode 100644 index 0000000..33e809e --- /dev/null +++ b/chapter11/kilhyeonjun/code/exercises/11.2-callback-batch-cache.js @@ -0,0 +1,215 @@ +/** + * 11.2-callback-batch-cache.js + * 연습문제 11.2: 콜백 기반 배치 및 캐싱 + * + * Promise 없이 순수 콜백 방식으로 + * 요청 배치 및 캐싱 구현 + */ + +/** + * 콜백 기반 배치 래퍼 + * + * @param {Function} originalFn - 원본 비동기 함수 (key, callback) + * @returns {Function} 배치가 적용된 함수 + */ +function createBatchedCallback(originalFn) { + // 진행 중인 요청 저장: key -> [callback1, callback2, ...] + const pendingRequests = new Map() + + return function batchedFn(key, callback) { + // 이미 진행 중인 요청이 있으면 콜백만 추가 + if (pendingRequests.has(key)) { + console.log(`[Batch] Piggybacking on: ${key}`) + pendingRequests.get(key).push(callback) + return + } + + // 새 요청 시작 + console.log(`[Batch] Starting new request: ${key}`) + pendingRequests.set(key, [callback]) + + originalFn(key, (err, result) => { + // 대기 중인 모든 콜백 호출 + const callbacks = pendingRequests.get(key) + pendingRequests.delete(key) + + console.log(`[Batch] Notifying ${callbacks.length} callbacks for: ${key}`) + callbacks.forEach(cb => cb(err, result)) + }) + } +} + +/** + * 콜백 기반 캐시 + 배치 래퍼 + * + * @param {Function} originalFn - 원본 비동기 함수 (key, callback) + * @param {number} ttlMs - 캐시 TTL (밀리초) + * @returns {Object} { cachedFn, invalidate, invalidateAll } + */ +function createCachedCallback(originalFn, ttlMs = 5000) { + const pendingRequests = new Map() + const cache = new Map() + + function cachedFn(key, callback) { + // 1. 캐시 확인 + const cached = cache.get(key) + if (cached) { + const age = Date.now() - cached.timestamp + if (age < ttlMs) { + console.log(`[Cache] HIT for ${key} (age: ${age}ms)`) + // 비동기로 콜백 호출 (Zalgo 방지) + setImmediate(() => callback(null, cached.value)) + return + } + console.log(`[Cache] EXPIRED for ${key}`) + cache.delete(key) + } + + // 2. 배치 확인 + if (pendingRequests.has(key)) { + console.log(`[Batch] Piggybacking on: ${key}`) + pendingRequests.get(key).push(callback) + return + } + + // 3. 새 요청 + console.log(`[Cache] MISS for ${key}`) + pendingRequests.set(key, [callback]) + + originalFn(key, (err, result) => { + const callbacks = pendingRequests.get(key) + pendingRequests.delete(key) + + // 성공 시 캐시에 저장 + if (!err) { + cache.set(key, { + value: result, + timestamp: Date.now() + }) + } + + callbacks.forEach(cb => cb(err, result)) + }) + } + + // 캐시 무효화 + function invalidate(key) { + console.log(`[Cache] Invalidating: ${key}`) + cache.delete(key) + } + + function invalidateAll() { + console.log(`[Cache] Invalidating all`) + cache.clear() + } + + return { cachedFn, invalidate, invalidateAll } +} + +// 테스트용 느린 API (콜백 기반) +function slowApiCallback(key, callback) { + console.log(`[API] Request started for: ${key}`) + setTimeout(() => { + console.log(`[API] Request completed for: ${key}`) + callback(null, { + key, + data: `Result for ${key}`, + timestamp: Date.now() + }) + }, 500) +} + +// 테스트 +function runTests() { + console.log('=== Callback-based Batch & Cache Demo ===\n') + + // 1. 배치만 적용 + console.log('--- Test 1: Batching Only ---') + const batchedApi = createBatchedCallback(slowApiCallback) + + let completed = 0 + const checkComplete = (expected, next) => { + return (err, result) => { + completed++ + console.log(`[Result] Received:`, result?.key) + if (completed === expected && next) { + setTimeout(next, 100) + } + } + } + + // 동시에 3개의 동일한 요청 + batchedApi('user:1', checkComplete(3)) + batchedApi('user:1', checkComplete(3)) + batchedApi('user:1', checkComplete(3, testCaching)) + + // 2. 캐시 + 배치 적용 + function testCaching() { + console.log('\n--- Test 2: Caching + Batching ---') + completed = 0 + + const { cachedFn, invalidate } = createCachedCallback(slowApiCallback, 2000) + + // 첫 번째 호출 (캐시 미스) + cachedFn('user:2', (err, result) => { + console.log('[Result 1] First call:', result?.key) + + // 두 번째 호출 (캐시 히트) + cachedFn('user:2', (err, result) => { + console.log('[Result 2] Second call (should be cached):', result?.key) + + // 캐시 무효화 후 세 번째 호출 + invalidate('user:2') + cachedFn('user:2', (err, result) => { + console.log('[Result 3] Third call (after invalidation):', result?.key) + + testConcurrent() + }) + }) + }) + } + + // 3. 동시 요청 + 캐시 + function testConcurrent() { + console.log('\n--- Test 3: Concurrent Requests with Cache ---') + completed = 0 + + const { cachedFn } = createCachedCallback(slowApiCallback, 5000) + + const done = (label) => (err, result) => { + completed++ + console.log(`[${label}] Got:`, result?.key) + if (completed === 4) { + console.log('\n=== All Tests Complete ===') + } + } + + // 동시 요청 - 첫 번째는 API 호출, 나머지는 배치 + cachedFn('user:3', done('Request A')) + cachedFn('user:3', done('Request B')) + cachedFn('user:3', done('Request C')) + + // 약간의 딜레이 후 요청 - 캐시 히트 + setTimeout(() => { + cachedFn('user:3', done('Request D (delayed)')) + }, 600) + } +} + +runTests() + +/** + * 콜백 기반 구현의 특징: + * + * 1. Promise 없이 동작 + * - 레거시 코드와 호환 + * - 낮은 오버헤드 + * + * 2. Zalgo 방지 + * - 캐시 히트 시에도 setImmediate로 비동기 호출 + * - 일관된 비동기 동작 보장 + * + * 3. 콜백 배열 관리 + * - Map에 콜백 배열 저장 + * - 완료 시 모든 콜백에 결과 전달 + */ diff --git a/chapter11/kilhyeonjun/code/exercises/11.3-deep-cancelable.js b/chapter11/kilhyeonjun/code/exercises/11.3-deep-cancelable.js new file mode 100644 index 0000000..7f32bd3 --- /dev/null +++ b/chapter11/kilhyeonjun/code/exercises/11.3-deep-cancelable.js @@ -0,0 +1,283 @@ +/** + * 11.3-deep-cancelable.js + * 연습문제 11.3: Deep 취소 가능한 비동기 함수 + * + * 중첩된 취소 가능 함수에서 루트 함수 취소 시 + * 모든 중첩 함수까지 취소되는 기능 구현 + */ + +// 취소 에러 +class CancelError extends Error { + constructor(message = 'Operation cancelled') { + super(message) + this.name = 'CancelError' + } +} + +/** + * Deep 취소 가능한 비동기 함수 생성기 + * + * 취소 컨텍스트를 자식 함수에 전파하여 + * 중첩된 모든 함수가 취소될 수 있도록 함 + */ +class CancelContext { + constructor(parent = null) { + this.parent = parent + this.children = new Set() + this.cancelRequested = false + this.cancelCallbacks = new Set() + + // 부모에 등록 + if (parent) { + parent.children.add(this) + // 부모가 이미 취소됐으면 자식도 취소 + if (parent.cancelRequested) { + this.cancelRequested = true + } + } + } + + // 취소 요청 + cancel() { + if (this.cancelRequested) return + + this.cancelRequested = true + console.log(`[Cancel] Context cancelled, propagating to ${this.children.size} children`) + + // 콜백 실행 + this.cancelCallbacks.forEach(cb => cb()) + + // 자식들도 취소 + this.children.forEach(child => child.cancel()) + } + + // 취소 콜백 등록 + onCancel(callback) { + if (this.cancelRequested) { + callback() + return + } + this.cancelCallbacks.add(callback) + return () => this.cancelCallbacks.delete(callback) + } + + // 취소 확인 + throwIfCancelled() { + if (this.cancelRequested) { + throw new CancelError() + } + } + + // 자식 컨텍스트 생성 + createChild() { + return new CancelContext(this) + } + + // 정리 + cleanup() { + if (this.parent) { + this.parent.children.delete(this) + } + } +} + +/** + * Deep 취소 가능한 비동기 함수 래퍼 + */ +function createDeepCancelable(asyncFn) { + return function(ctx = new CancelContext()) { + const promise = (async () => { + try { + return await asyncFn(ctx) + } finally { + ctx.cleanup() + } + })() + + return { + promise, + cancel: () => ctx.cancel(), + context: ctx + } + } +} + +// 비동기 작업 시뮬레이션 +async function delay(ms, ctx) { + return new Promise((resolve, reject) => { + const timer = setTimeout(resolve, ms) + + // 취소 시 타이머 정리 + ctx.onCancel(() => { + clearTimeout(timer) + reject(new CancelError()) + }) + }) +} + +// 예제: 중첩된 취소 가능 함수들 +const innerOperation = createDeepCancelable(async (ctx) => { + console.log('[Inner] Starting...') + ctx.throwIfCancelled() + + await delay(300, ctx) + console.log('[Inner] Step 1 done') + + ctx.throwIfCancelled() + await delay(300, ctx) + console.log('[Inner] Step 2 done') + + return 'Inner result' +}) + +const middleOperation = createDeepCancelable(async (ctx) => { + console.log('[Middle] Starting...') + ctx.throwIfCancelled() + + await delay(200, ctx) + console.log('[Middle] Step 1 done') + + // 자식 컨텍스트로 내부 작업 호출 + const childCtx = ctx.createChild() + const { promise: innerPromise } = innerOperation(childCtx) + const innerResult = await innerPromise + console.log('[Middle] Inner result:', innerResult) + + ctx.throwIfCancelled() + await delay(200, ctx) + console.log('[Middle] Step 2 done') + + return 'Middle result' +}) + +const outerOperation = createDeepCancelable(async (ctx) => { + console.log('[Outer] Starting...') + ctx.throwIfCancelled() + + await delay(100, ctx) + console.log('[Outer] Step 1 done') + + // 자식 컨텍스트로 중간 작업 호출 + const childCtx = ctx.createChild() + const { promise: middlePromise } = middleOperation(childCtx) + const middleResult = await middlePromise + console.log('[Outer] Middle result:', middleResult) + + ctx.throwIfCancelled() + await delay(100, ctx) + console.log('[Outer] Step 2 done') + + return 'Outer result' +}) + +// 테스트 +async function main() { + console.log('=== Deep Cancelable Demo ===\n') + + // Test 1: 정상 완료 + console.log('--- Test 1: Normal completion ---') + try { + const { promise, cancel, context } = outerOperation() + const result = await promise + console.log('Final result:', result) + } catch (err) { + console.log('Error:', err.message) + } + + console.log('\n--- Test 2: Cancel from root (after 500ms) ---') + try { + const { promise, cancel, context } = outerOperation() + + // 500ms 후 취소 (Inner 실행 중에 취소됨) + setTimeout(() => { + console.log('\n[Main] Requesting cancellation...') + cancel() + }, 500) + + const result = await promise + console.log('Final result:', result) + } catch (err) { + if (err instanceof CancelError) { + console.log('[Main] Operation was cancelled successfully') + } else { + throw err + } + } + + console.log('\n--- Test 3: Cancel immediately ---') + try { + const { promise, cancel } = outerOperation() + + // 즉시 취소 + cancel() + + const result = await promise + console.log('Final result:', result) + } catch (err) { + if (err instanceof CancelError) { + console.log('[Main] Immediate cancellation worked') + } else { + throw err + } + } + + // Test 4: 병렬 중첩 작업 + console.log('\n--- Test 4: Parallel nested operations ---') + + const parallelOperation = createDeepCancelable(async (ctx) => { + console.log('[Parallel] Starting 3 concurrent tasks...') + + const child1 = ctx.createChild() + const child2 = ctx.createChild() + const child3 = ctx.createChild() + + const results = await Promise.all([ + innerOperation(child1).promise.catch(e => `Task 1: ${e.message}`), + innerOperation(child2).promise.catch(e => `Task 2: ${e.message}`), + innerOperation(child3).promise.catch(e => `Task 3: ${e.message}`) + ]) + + return results + }) + + try { + const { promise, cancel } = parallelOperation() + + setTimeout(() => { + console.log('\n[Main] Cancelling parallel operations...') + cancel() + }, 400) + + const result = await promise + console.log('Parallel results:', result) + } catch (err) { + if (err instanceof CancelError) { + console.log('[Main] Parallel operation cancelled') + } else { + throw err + } + } +} + +main().catch(console.error) + +/** + * Deep 취소의 핵심: + * + * 1. CancelContext 계층 구조 + * - 부모-자식 관계로 컨텍스트 연결 + * - 부모 취소 시 모든 자식에게 전파 + * + * 2. 취소 콜백 + * - 진행 중인 비동기 작업(타이머, fetch 등) 정리 + * - 리소스 누수 방지 + * + * 3. 명시적 취소 확인 + * - throwIfCancelled()로 취소 포인트 생성 + * - 각 단계에서 취소 확인 + * + * 확장 가능: + * - AbortController와 통합 + * - 취소 이유 전달 + * - 부분 취소 (특정 자식만) + */ diff --git a/chapter11/kilhyeonjun/code/exercises/11.4-computing-farm.js b/chapter11/kilhyeonjun/code/exercises/11.4-computing-farm.js new file mode 100644 index 0000000..01bf076 --- /dev/null +++ b/chapter11/kilhyeonjun/code/exercises/11.4-computing-farm.js @@ -0,0 +1,408 @@ +/** + * 11.4-computing-farm.js + * 연습문제 11.4: 컴퓨팅 팜 + * + * HTTP로 작업을 분산하고 vm 모듈로 동적 코드를 실행하는 + * 분산 컴퓨팅 시스템 구현 + */ + +import { createServer } from 'http' +import { request as httpRequest } from 'http' +import vm from 'vm' + +// ============================================ +// Worker Node (작업자 노드) +// ============================================ + +class WorkerNode { + constructor(port) { + this.port = port + this.taskCount = 0 + this.server = null + } + + start() { + this.server = createServer((req, res) => { + if (req.method === 'POST' && req.url === '/execute') { + this._handleExecute(req, res) + } else if (req.method === 'GET' && req.url === '/status') { + this._handleStatus(req, res) + } else { + res.writeHead(404) + res.end('Not Found') + } + }) + + this.server.listen(this.port, () => { + console.log(`[Worker] Listening on port ${this.port}`) + }) + } + + async _handleExecute(req, res) { + let body = '' + for await (const chunk of req) { + body += chunk + } + + try { + const { code, context = {} } = JSON.parse(body) + this.taskCount++ + + console.log(`[Worker:${this.port}] Executing task #${this.taskCount}`) + const startTime = Date.now() + + // vm으로 안전하게 코드 실행 + const result = await this._executeCode(code, context) + const duration = Date.now() - startTime + + console.log(`[Worker:${this.port}] Task completed in ${duration}ms`) + + res.writeHead(200, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ + success: true, + result, + duration, + worker: this.port + })) + } catch (error) { + console.error(`[Worker:${this.port}] Error:`, error.message) + res.writeHead(500, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ + success: false, + error: error.message, + worker: this.port + })) + } + } + + _handleStatus(req, res) { + res.writeHead(200, { 'Content-Type': 'application/json' }) + res.end(JSON.stringify({ + port: this.port, + taskCount: this.taskCount, + uptime: process.uptime() + })) + } + + async _executeCode(code, contextData) { + // 샌드박스 컨텍스트 생성 + const sandbox = { + console: { + log: (...args) => console.log(`[Sandbox]`, ...args) + }, + setTimeout, + Promise, + Array, + Object, + Math, + JSON, + ...contextData, + result: undefined + } + + // 컨텍스트 생성 + const context = vm.createContext(sandbox) + + // 타임아웃과 함께 코드 실행 + const script = new vm.Script(` + (async () => { + ${code} + })() + `) + + const result = await script.runInContext(context, { + timeout: 5000 // 5초 타임아웃 + }) + + return result + } + + stop() { + if (this.server) { + this.server.close() + console.log(`[Worker:${this.port}] Stopped`) + } + } +} + +// ============================================ +// Farm Coordinator (팜 코디네이터) +// ============================================ + +class FarmCoordinator { + constructor(workerPorts) { + this.workers = workerPorts.map(port => ({ + port, + host: 'localhost', + busy: false, + taskCount: 0 + })) + this.taskQueue = [] + this.roundRobinIndex = 0 + } + + // 라운드 로빈 방식으로 워커 선택 + _selectWorker() { + const availableWorkers = this.workers.filter(w => !w.busy) + if (availableWorkers.length === 0) { + return null + } + + this.roundRobinIndex = (this.roundRobinIndex + 1) % availableWorkers.length + return availableWorkers[this.roundRobinIndex] + } + + // 가장 적게 사용된 워커 선택 + _selectLeastBusyWorker() { + const availableWorkers = this.workers.filter(w => !w.busy) + if (availableWorkers.length === 0) { + return null + } + + return availableWorkers.reduce((min, w) => + w.taskCount < min.taskCount ? w : min + ) + } + + // 작업 실행 + async execute(code, context = {}) { + const worker = this._selectLeastBusyWorker() + + if (!worker) { + // 모든 워커가 바쁘면 큐에 저장 + return new Promise((resolve, reject) => { + this.taskQueue.push({ code, context, resolve, reject }) + }) + } + + worker.busy = true + worker.taskCount++ + + try { + const result = await this._sendToWorker(worker, code, context) + return result + } finally { + worker.busy = false + this._processQueue() + } + } + + // 큐 처리 + _processQueue() { + if (this.taskQueue.length === 0) return + + const worker = this._selectLeastBusyWorker() + if (!worker) return + + const task = this.taskQueue.shift() + this.execute(task.code, task.context) + .then(task.resolve) + .catch(task.reject) + } + + // HTTP로 워커에 작업 전송 + _sendToWorker(worker, code, context) { + return new Promise((resolve, reject) => { + const data = JSON.stringify({ code, context }) + + const req = httpRequest({ + hostname: worker.host, + port: worker.port, + path: '/execute', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(data) + } + }, (res) => { + let body = '' + res.on('data', chunk => body += chunk) + res.on('end', () => { + try { + const result = JSON.parse(body) + if (result.success) { + resolve(result) + } else { + reject(new Error(result.error)) + } + } catch (e) { + reject(e) + } + }) + }) + + req.on('error', reject) + req.write(data) + req.end() + }) + } + + // 병렬 실행 + async executeParallel(tasks) { + return Promise.all( + tasks.map(task => this.execute(task.code, task.context)) + ) + } + + // 맵-리듀스 실행 + async mapReduce(data, mapCode, reduceCode) { + // Map 단계: 각 데이터 청크를 병렬 처리 + const chunks = this._splitArray(data, this.workers.length) + const mapTasks = chunks.map(chunk => ({ + code: mapCode, + context: { data: chunk } + })) + + console.log(`[Farm] Map phase: ${mapTasks.length} tasks`) + const mapResults = await this.executeParallel(mapTasks) + + // Reduce 단계: 결과 합치기 + console.log(`[Farm] Reduce phase`) + const reduceResult = await this.execute(reduceCode, { + results: mapResults.map(r => r.result) + }) + + return reduceResult + } + + _splitArray(arr, n) { + const chunks = [] + const chunkSize = Math.ceil(arr.length / n) + for (let i = 0; i < arr.length; i += chunkSize) { + chunks.push(arr.slice(i, i + chunkSize)) + } + return chunks + } + + // 상태 조회 + async getStatus() { + const statuses = await Promise.all( + this.workers.map(w => + new Promise((resolve) => { + const req = httpRequest({ + hostname: w.host, + port: w.port, + path: '/status', + method: 'GET' + }, (res) => { + let body = '' + res.on('data', chunk => body += chunk) + res.on('end', () => { + try { + resolve(JSON.parse(body)) + } catch { + resolve({ port: w.port, error: 'Parse error' }) + } + }) + }) + req.on('error', () => resolve({ port: w.port, error: 'Unreachable' })) + req.end() + }) + ) + ) + return statuses + } +} + +// ============================================ +// 데모 실행 +// ============================================ + +async function main() { + console.log('=== Computing Farm Demo ===\n') + + // 워커 노드 시작 + const workerPorts = [3001, 3002, 3003] + const workers = workerPorts.map(port => new WorkerNode(port)) + workers.forEach(w => w.start()) + + // 워커가 시작될 때까지 대기 + await new Promise(resolve => setTimeout(resolve, 500)) + + // 팜 코디네이터 생성 + const farm = new FarmCoordinator(workerPorts) + + try { + // 1. 단순 작업 실행 + console.log('\n--- Test 1: Simple execution ---') + const result1 = await farm.execute(` + const sum = Array.from({ length: 100 }, (_, i) => i + 1) + .reduce((a, b) => a + b, 0); + return sum; + `) + console.log('Result:', result1) + + // 2. 컨텍스트와 함께 실행 + console.log('\n--- Test 2: With context ---') + const result2 = await farm.execute(` + const doubled = numbers.map(n => n * multiplier); + return doubled; + `, { + numbers: [1, 2, 3, 4, 5], + multiplier: 10 + }) + console.log('Result:', result2) + + // 3. 병렬 실행 + console.log('\n--- Test 3: Parallel execution ---') + const tasks = [ + { code: 'return "Task A: " + Math.random()' }, + { code: 'return "Task B: " + Math.random()' }, + { code: 'return "Task C: " + Math.random()' } + ] + const results = await farm.executeParallel(tasks) + console.log('Results:', results.map(r => r.result)) + + // 4. Map-Reduce + console.log('\n--- Test 4: Map-Reduce ---') + const data = Array.from({ length: 100 }, (_, i) => i + 1) + + const mapResult = await farm.mapReduce( + data, + // Map: 각 청크의 합계 + `return data.reduce((a, b) => a + b, 0)`, + // Reduce: 모든 합계의 합 + `return results.reduce((a, b) => a + b, 0)` + ) + console.log('Map-Reduce result:', mapResult.result) + console.log('Expected:', data.reduce((a, b) => a + b, 0)) + + // 5. 상태 확인 + console.log('\n--- Worker Status ---') + const status = await farm.getStatus() + console.log(status) + + } finally { + // 정리 + workers.forEach(w => w.stop()) + } +} + +main().catch(console.error) + +/** + * 컴퓨팅 팜의 특징: + * + * 1. HTTP 기반 분산 + * - 워커 노드가 HTTP 서버로 동작 + * - 네트워크를 통한 작업 분배 + * - 언어/플랫폼 독립적 확장 가능 + * + * 2. vm 기반 안전한 실행 + * - 샌드박스 환경에서 코드 실행 + * - 타임아웃 설정으로 무한 루프 방지 + * - 제한된 API만 노출 + * + * 3. 로드 밸런싱 + * - 라운드 로빈 / 최소 사용 선택 + * - 작업 큐로 과부하 방지 + * + * 4. Map-Reduce 패턴 + * - 대규모 데이터 병렬 처리 + * - 자동 청크 분할 + * + * 보안 주의사항: + * - 프로덕션에서는 더 엄격한 샌드박싱 필요 + * - 인증/인가 추가 필요 + * - 리소스 제한 (메모리, CPU) 필요 + */