Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions chapter08/생각.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
## Proxy && Decorator

노드 진영에서의 proxy와 데코레이터는 거의 동치라고 봐도 된다고 한다. 아래 예시로 존재한다.

```ts
🎯 Node.js에서는 둘 다 “그냥 함수 감싸기”
데코레이터 예 (JS)
function deco(fn) {
return (...args) => {
console.log("장식 추가");
return fn(...args);
};
}
프록시 예 (JS)
const proxy = new Proxy(obj, {
get(target, prop) {
console.log("대리 처리");
return target[prop];
}
});

👉 둘 다 겉에서 감싸서 가로채는 방식
👉 개발자 마음대로 조합할 수 있어서 구분이 모호해짐
👉 언어적인 차이가 없어서 사실상 “같은 종류의 기술”
```

```java
// 🎯 Java에서는 구조가 완전 다름
// ⭐ 데코레이터는 “겉에 스킨 씌우기”
class Decorator implements Service {
private Service next;
public Decorator(Service next) { this.next = next; }

public void run() {
System.out.println("장식 추가");
next.run();
}
}
// ⭐ 프록시는 “대리기사” 규칙을 따라야만 함
Service proxy = (Service) Proxy.newProxyInstance(
Service.class.getClassLoader(),
new Class[]{Service.class},
(proxy, method, args) -> {
System.out.println("대리 처리");
return method.invoke(realService, args);
}
);
➡ 완전히 다른 문법
➡ 완전히 다른 구조
➡ Java에서는 누가 봐도 구분됨

👍 한 문장으로 극단적 요약

🟦 Node.js
둘 다 “그냥 감싸기”라서 구분하기가 사실 불가능함.
(동적 언어라 경계가 거의 없음)

🟥 Java
데코레이터는 클래스 구조, 프록시는 인터페이스 기반 대리 호출 구조라 서로 완전 다름.
(정적 언어라 구조가 명확)
```

## Adapter

내 기억대로 책에서도 실제 존재하는 구현체의 사용 방식을 바꿔버리는 방법을 소개한다.

- 사람들이 헷갈려하는 이유 중 하나가 자꾸 "인터페이스"라해서 그런데, 이는 OOP Interface가 아니라 좀 더 범용적인, 그냥 사용 방법 측면에서 어떤 함수가 있냐는 의미의 인터페이스이다 (User Interface와 가까운 이야기)
- 그래서 clean, hexagonal, ddd 에 나오는 어댑터 패턴이랑은 확연히 다르다. 거기는 포트에 여러 어댑터를 끼우는 패턴이며 이는 포트에 어댑터가 어느정도 맞추어주어야할 수 있다. 즉, 어댑터가 좀 더 쓰임을 당하는 입장이다.
- 그런데, gof 어댑터 패턴은 방향이 반대이다. 원본 클래스/인터페이스가 있고, 이를 어댑터가 쓰고 싶은대로 새로운 인터페이스를 만드는 것이다.
- 즉, 의존 관점에서 방향성이 신기하게도 완전히 정반대의 패턴인 것이다. gof 어댑터는 주체를 갖고 있고, hexagonal 어댑터는 쓰임을 당하는 느낌이 강하다.
210 changes: 210 additions & 0 deletions chapter11/생각.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
## <1> Asynchronous request batching and caching

```ts
import { totalSales as totalSalesRaw } from "./totalSales.js";

const runningRequests = new Map();

export function totalSales(product) {
if (runningRequests.has(product)) {
console.log("Batching");
return runningRequests.get(product);
}
const resultPromise = totalSalesRaw(product);
runningRequests.set(product, resultPromise);
resultPromise.finally(() => {
runningRequests.delete(product);
});

return resultPromise;
}
```

- 돌고 있는 promise를 map에 갖고 있으면 반환은 쉽다. key를 제품 id로 걸어두면 언제든 실행중인지 확인 가능하다. 동일 프로미스로 반환 가능하다.

```ts
import { totalSales as totalSalesRaw } from "./totalSales.js";

const CACHE_TTL = 30 * 1000; // 30초 TTL
const cache = new Map();

export function totalSales(product) {
if (cache.has(product)) {
console.log("Cache hit");
return cache.get(product);
}

const resultPromise = totalSalesRaw(product);

cache.set(product, resultPromise);

resultPromise
.then(() => {
setTimeout(() => {
cache.delete(product);
}, CACHE_TTL);
})
.catch((err) => {
cache.delete(product);
throw err;
});

return resultPromise;
}
```

- 캐싱도 프로미스로 하는 예시인데....
- 배칭과 캐싱은 같이 둘 수 있다.
- 근데 위 예시는 과연 이상적인 것일까..? setTimeout은 memory leatk에 꽤 취약한데...
- hacky해보이는 접근. 그냥 따로 스케쥴러 돌려서 클린업하든지 하는게 나을듯함.
- 보통 caching은 fine grained control을 타겟으로 하는건데, 굳이 위처럼 프로미스로 관리하는 건 그리 바람직하지 않은듯함.

내생각: 배칭과 캐싱을 함께 쓰면 - 캐시 스탬피드를 막기 좋아보임.

## <2> A basic recipe for creating cancelable functions

```ts
import { asyncRoutine } from "./asyncRoutine.js";
import { CancelError } from "./cancelError.js";

// cancelObj 는 { cancelRequested: boolean } 형태의 객체여야 함.
// 각 단계마다 cancelRequested를 확인하여 작업 취소를 지원합니다.
export async function cancelable(cancelObj) {
const resA = await asyncRoutine("A");
console.log(resA);
if (cancelObj.cancelRequested) {
throw new CancelError();
}

const resB = await asyncRoutine("B");
console.log(resB);
if (cancelObj.cancelRequested) {
throw new CancelError();
}

const resC = await asyncRoutine("C");
console.log(resC);
}
```

- 책에서 소개하는 도중 캔슬의 방법: event loop 마다 사이사이에 cancel되어있는지 확인 하는 방법.

```ts
import { asyncRoutine } from "./asyncRoutine.js";
import { createAsyncCancelable } from "./createAsyncCancelable.js";
import { CancelError } from "./cancelError.js";

const cancelable = createAsyncCancelable(function* () {
const resA = yield asyncRoutine("A");
console.log(resA);
const resB = yield asyncRoutine("B");
console.log(resB);
const resC = yield asyncRoutine("C");
console.log(resC);
});

const { promise, cancel } = cancelable();

promise.catch((err) => {
if (err instanceof CancelError) {
console.log("Function canceled");
} else {
console.error(err);
}
});

setTimeout(() => {
cancel();
}, 1000);
```

- 더 우아하게 하도록 소개된 방법: generator을 써라. => yield로 핸들링하여 cancel자체를 래핑해서 함수 내에서 관심사를 타 함수로 분리할 수 있음.

## <3> interleaving 대신 child_process or worker 쓰기

- interleaving 써서 작업 중간 중간 휴식을 주어서 동시성에 강하게 할 수는 있음. (responsiveness 강화)
- 하지만, 근본적으로 cpu 작업이 많아지면 전체 총 최종 응답까지 걸리는 시간은 현저하게 느려짐.
- worker thread를 써서 소통하고 속도를 올려라.

| 항목 | `child_process` | `Worker Threads` (`worker_threads`) |
| ----------- | ------------------------------------ | ------------------------------------------ |
| 목적 | **프로세스 단위 분리** → 독립 실행 | **스레드 단위 병렬 처리** → CPU-bound 옵티 |
| 메모리 공간 | 완전 독립 | 메모리 공유 가능(`SharedArrayBuffer`) |
| 통신 방식 | IPC (직렬화 필요) | MessageChannel / 공유메모리 활용 |
| 비용 | 프로세스 생성 비용 큼 | 스레드 생성이 더 저렴 |
| 장점 | Crash isolation, 다른 언어 실행 유리 | CPU 연산 병렬 처리 최적 |
| 적합한 작업 | 대형 서비스·별도 앱 실행 | 해시/압축/이미지처리 등의 Heavy CPU Work |

worker thread가 무조건 가볍고 좋은거 아닌가? node 네이티브하게 지원되는 멀티스레드인데...

## <4> worker thread보다 child_process가 월등한 상황들 (gpt 5.1 생성 + 약간의 수정)

- 1번은 tsbm 모임에도 발표로 나왔었습니다. (ffmpeg 처리)

### 1) **다른 언어 실행이 필요할 때**

Node Worker는 **JS/TS만 실행 가능**합니다. 하지만 child_process는 아래 모두 가능합니다.

| 실행 가능 | 예시 |
| ------------------------------ | ---------------------------- |
| Python | ML 모델, Numpy, Scikit-Learn |
| Go / Rust / C++ | 고성능 바이너리 실행 |
| FFmpeg / OpenSSL / ImageMagick | CLI 기반 미디어 처리 |

```js
spawn("python3", ["script.py"]);
```

👆 Worker Threads로는 절대 불가.
➡ **외부 프로그램 실행 = child_process가 정답**

---

### 2) **프로세스 크래시가 메인을 죽이면 안 될 때**

Worker Threads는 **같은 프로세스에서 돌아감 → crash 시 메인도 위험**
반면 child_process는 완전히 분리된 독립 실행.

| 비교 | Worker | child_process |
| ------------- | ---------- | ------------- |
| 메모리 공유 | O | X |
| 크래시 고립성 | 약함 | 매우 높음 |
| 메인에 영향 | 줄 수 있음 | 거의 없음 |

SaaS나 Backend 서비스에서 안정성을 중시한다면 → **프로세스 분리 전략이 필수적**

---

### 3) **CPU + Memory 자원을 완전히 고립하여 운영하고 싶을 때**

별도 프로세스이므로 다음이 가능:

| 지원 | worker_threads | child_process |
| ----------- | -------------- | --------------------------------------- |
| CPU 제한 | ❌ | cgroup, scheduler 이용 가능 |
| Memory 제한 | 높지 않음 | `--max-old-space-size` 등으로 제한 가능 |

즉 **자원 할당량 제어가 필요한 배치/작업 서버**에서 child_process가 유리하다.

---

### 정리하면

> 🚀 **Worker Threads** = Node 내부 병렬 처리(CPU 연산용)
> 🔥 **child_process** = 별도 실행환경, 외부 프로그램 실행, 장애 격리

즉 "멀티스레딩이 필요해서" child_process를 쓰지 않는다.
child_process는 **아키텍처 관점에서 더 큰 스케일링을 지원하는 도구**다.

---

### ✔ 쓸 이유가 명확함

| Yes | Worker Threads 대신 child_process |
| ---------------------------- | --------------------------------- |
| 외부 언어/바이너리 실행할 때 | Python, FFmpeg, C++ |
| 장애 격리 필요 | Crash safe |
| 자원 제한·모듈 격리 필요 | CPU/Memory 정책 적용 가능 |

**CPU 병렬화만 원하면 → worker_threads**
**완전한 독립성/격리/외부 실행이 필요하면 → child_process**
Loading