diff --git "a/chapter08/\354\203\235\352\260\201.md" "b/chapter08/\354\203\235\352\260\201.md" new file mode 100644 index 0000000..cd570ff --- /dev/null +++ "b/chapter08/\354\203\235\352\260\201.md" @@ -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 어댑터는 쓰임을 당하는 느낌이 강하다. diff --git "a/chapter11/\354\203\235\352\260\201.md" "b/chapter11/\354\203\235\352\260\201.md" new file mode 100644 index 0000000..d27d075 --- /dev/null +++ "b/chapter11/\354\203\235\352\260\201.md" @@ -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** diff --git "a/chapter13/\354\203\235\352\260\201.md" "b/chapter13/\354\203\235\352\260\201.md" new file mode 100644 index 0000000..eef09b4 --- /dev/null +++ "b/chapter13/\354\203\235\352\260\201.md" @@ -0,0 +1,251 @@ +## 왜 갑자기 메세징? + +- node의 한계점 - CPU bound 작업에 약하다. +- node는 필연적으로 다른 언어들보다 여러개의 서버를 띄워서 마이크로서비스 형태로 서빙할 가능성이 높음. +- 그래서 메세징 관련된 것을 가르치는 것으로 보여짐. + +## 왜 찹터에 zeromq 랑 rabbitmq가 자꾸 나옴? zeromq가 무슨 듣보잡임? + +broker vs p2p 패턴을 설명하고자 자꾸 등장하는 것임. (이런 선택지가 있고, 설계가 이렇게 달라진다) + +zeromq는 node에서 p2p messaging 구현하는 가장 대표적인 방법. 레이턴시 낮아야하는 금융, 게임 서버, 분산 컴퓨팅에서 유명. (성능/제어 최우선) zeromq는 브로커가 아님. 그냥 p2p 메시징 라이브러리임. + +rabbitmq는 node에서 broker messaging 구현하는 가장 대표적인 방법. 전통적 메세지 브로커. + +세 번째 축으로 redis도 등장함. redis pub sub은 사실상 신뢰성 있는 메세징 시스템은 아님. redis stream은 신뢰성 있는 append-only log 기반 메세징은 맞음. kafka-lite에 가까움. 비전통적 메세지 브로커. + +## 9가지 조합. + +1. 메시지 교환 패턴 (What / Semantics) + +- Publish / Subscribe +- Task Distribution +- Request / Reply + +2. 아키텍처 방식 (How / Topology) + +- Peer-to-peer +- Broker-based +- Broker-based (Stream) + +교환 패턴과 아키텍처 조합하면 9가지가 나옴. + +| 조합 | 메시지 패턴 | 아키텍처 | 사용 빈도 | 주요 용도 | 대표 기술 | +| ---- | ------------------- | --------------- | ---------- | -------------------------------- | ------------------------------------ | +| 1 | Publish / Subscribe | Broker | ⭐⭐⭐⭐⭐ | 이벤트 전파, 상태 변경 알림 | Kafka, Redis Pub/Sub, RabbitMQ | +| 2 | Task Distribution | Broker | ⭐⭐⭐⭐⭐ | 비동기 작업, 백그라운드 처리 | SQS, RabbitMQ, BullMQ | +| 3 | Request / Reply | Peer-to-peer | ⭐⭐⭐⭐ | API 호출, DB 쿼리 | HTTP, REST, gRPC | +| 4 | Publish / Subscribe | Broker (Stream) | ⭐⭐⭐ | 이벤트 스트리밍, 리플레이 | Kafka, Pulsar, Kinesis, Redis Stream | +| 5 | Request / Reply | Broker | ⭐⭐ | 비동기 요청/응답 | Reply Queue, Correlation ID | +| 6 | Task Distribution | Peer-to-peer | ❌ | 워커 관리 어려움, 실패 처리 난해 | ZeroMQ PUSH/PULL | +| 7 | Publish / Subscribe | Peer-to-peer | ❌ | 구독자 관리 불가능, 확장성 낮음 | ZeroMQ PUB/SUB | +| 8 | Task Distribution | Broker (Stream) | ❌ | 로그를 제어 흐름으로 = 타협적 | Kafka, Pulsar, Kinesis, Redis Stream | +| 9 | Request / Reply | Broker (Stream) | ❌ | latency, coupling, 복잡도 | Kafka, Pulsar, Kinesis, Redis Stream | + +핵심 인사이트 + +- 1번에서의 rabbitmq와 2번에서의 rabbitmq의 차이점: 1번에서는 fan-out을 하고, 2번에서는 한 번 소비되면 사라짐. (n번 소비되지 않음.) => task distribution인지 바로 와닿는 대목. => 1번에서는 exchange + multi queue, 2번에서는 하나의 queue만 사용. +- 3번은 우리가 알던 그 http가 맞음. +- 4번 stream은 이름 때문에 헷갈리는데, gRPC 같은 거에 스트림으로 연결받는 걸 말하는게 아님. + - Stream = 삭제되지 않는 메시지 로그를 시간순으로 계속 쌓아두는 것 (Message Stream) + - 그냥 pubsub이랑 도대체 무슨 차이죠? => 일반적인 pub/sub은 히스토리를 보장하지 않는 경우가 많다. stream은 특정 시간의 과거로 부터 다시 히스토리를 읽어서 이벤트를 모두 리플레이할 수 있음. + +## 책 예시를 모아보자 + +### Using Redis as a simple message broker (1번) + +- redis pubsub은 일부러 아주 가볍게 설계된 시스템이다. + - 이미 캐싱에 쓰고 있는 회사 많아서 쓰기 부담이 적다. + - 아주 쉬운 구조를 제공하기에 부담이 적다. + - 그래서 인기가 많다. + +### Peer-to-peer Publish/Subscribe with ZeroMQ (7번) + +- 이론 소개로는 맞지만, 실무 적용 범위는 매우 좁음. +- '통제된 환경의 고성능 이벤트 배포’로 쓴다고함. +- 위에 표에서는 X처리되어있음. +- zeromq는 udp위에서 도나? 보통은 아님. + - tcp:// ✅ 기본 / 가장 많이 쓰임 + - ipc:// (Unix domain socket, 같은 머신) + - inproc:// (같은 프로세스 내) + - pgm:// / epgm:// (⚠️ 이게 UDP 기반) +- 하지만 tcp위에서 udp와 비슷하게 메세징을 함. + - ZeroMQ pub/sub의 구조: + - publisher는 구독자가 몇 명인지 모름 + - subscriber는 publisher가 살아있는지도 모름 + - 연결이 잠깐 끊기면: + - 메시지 유실 ✔ + - 재전송 ❌ + - 히스토리 ❌ + - 👉 일반적인 “이벤트 시스템”으로는 거의 사용 불가 + +### Implementing a history service using AMQP (1번) + +- ZeroMQ는 사실상 연결/메세지 처리 보장이 안되기 때문에 책에서 Queue를 써보자고 함. +- AMQP(rabbitmq의 프로토콜)는 훨씬 복잡하지만 메세지 처리 완료 ACK를 받을 떄까지 Queue에 남아있기 때문에 메세지 처리 보장이 됨. + +### Implementing the chat application using Redis Streams (4번) + +- Kafka와 Kinesis가 가장 유명하지만 가벼운 태스크 예시로는 Redis Streams가 좋다며 책에서는 Redis Stream 예시를 가져옴. +- 위에 섹션에서 언급한대로 Stream 방식은 MQ(AMQP) 방식과 많이 다름. 로그를 적어두는 방식임. +- Streams를 써야 할 때는 언제? + - 순서가 중요한 데이터 + - 과거를 다시 봐야 하는 경우 + - 배치 처리 / 상관관계 분석이 필요한 경우 +- 그럼 현대 아키텍처는 진짜 Kafka만 쓰나? + - ❌ 아니다 + - ⭕ Kafka + RabbitMQ (or SQS) 조합이 제일 흔함 + - 왜냐? + - Kafka의 본질 + - Event Log + - 데이터 파이프라인 + - 분석 / 스트리밍 / 리플레이 + - RabbitMQ의 본질 + - Work Queue + - 시스템 통합 + - 비즈니스 작업 실행 +- 현대 아키텍처 실제 그림 + + - 흔한 조합 + + ``` + [API 서버] + │ + ├─ (작업) → RabbitMQ / SQS → Worker + │ + └─ (이벤트) → Kafka → Analytics / Search / ML + ``` + + - 결제 완료 이벤트 → Kafka + - 이메일 발송 작업 → RabbitMQ + - 이미지 리사이즈 → Queue + - 클릭 로그 → Kafka + - 👉 이걸 전부 Kafka로 하면? + - 가능은 한데 + - 운영 지옥 + 개념 꼬임 + - “Kafka로 Task Queue 안 되냐?”에 대한 현실 답변 => 된다. 근데 그렇게 할 이유가 거의 없다. + - retry 직접 구현 + - DLQ 직접 구현 + - priority 불가 (Queue에서 당연한 것: 높은 우선순위 먼저, 일부 작업 5분 뒤에 처리 등) + - delay 불가 + - 그래서 실무에서 Kafka Task Queue는: + - “이미 Kafka밖에 없는 조직” + - “초대규모 단순 작업” + - 같은 특수 케이스임. + +- 그럼 왜 큰 회사들은 Kafka로 “다 해버리는 것처럼” 보일까? + + - 이유 1️⃣: 이벤트와 작업의 경계가 흐려짐 + - “결제 완료 이벤트” + - “결제 후 처리 작업” + - 👉 처음엔 이벤트, 그리고 그 뒤에 붙는 건 작업 + - 그래서 Kafka 하나로 묶어버림 + - 이유 2️⃣: 이미 Kafka가 중심에 있음 + - Kafka 운영팀 있음 + - 관측/모니터링 있음 + - ACL / 보안 있음 + - 👉 새로운 MQ 들이는 비용이 더 큼 + - 이유 3️⃣: 단순 작업은 Kafka로도 충분 + - priority 필요 없음 + - delay 필요 없음 + - 실패해도 재처리 가능 + - 👉 그럼 Kafka로 퉁침 + +- 내 생각: + + - kafka는 inbox, outbox 패턴으로 보통 구현되기 때문에 dlq가 애초에 필요 없긴함. + - kafka에서는 priority 구현을 그냥 토픽으로 해버린다고함. 실무에서 안써봐서 잘 모르겠음. + ``` + order.high + order.normal + order.low + ``` + - delay도 topic으로 구현 가능하긴한데, 훨씬 지저분함. + + ``` + task + → task.delay.1m + → task.delay.10m + → task.dlq + ``` + +- 결론: + Kafka가 어색해지는 순간: + - 긴 delay (몇 시간~며칠) + - 진짜 우선순위 (긴급 작업) + - 정밀한 재시도 정책 + - 👉 Queue 병행이 낫다 + +### Building a distributed hashsum cracker with ZeroMQ (6번) + +=> 노드에서 cpu bound한거 분리해서 처리하는거 보여주려고 cpu bound 작업을 예시로 만든듯함 (hashsum craker) + +- 어떤 예제? + - 큰 파일의 해시를 여러 워커에게 나눠서 계산 + - master ↔ worker 직접 통신 + - ZeroMQ PUSH / PULL 패턴 사용 +- 의도 + - "Broker 없이도 작업 분산은 가능하다" 보여주기 + - ZeroMQ의 성능/단순함 강조 +- 하지만... + - 워커 실패 시? + - 중복 실행? + - 재시도? + - 워커 수 늘어나면? + - 👉 전부 직접 구현해야 함 +- 핵심 메시지 + - P2P Task Distribution은 가능은 하지만 + - 실무에서는 거의 쓰지 않는다 + +### Implementing the hashsum cracker using AMQP (2번) + +- 뭐가 달라졌나? + - ZeroMQ → RabbitMQ + - worker는 queue에서 작업을 가져감 + - ACK 기반 처리 +- 얻는 것 + - 워커 죽으면 메시지 재분배 + - backpressure 자동 + - 스케일 아웃 쉬움 +- 핵심 메시지 + - 같은 문제라도 Broker를 쓰면 + - "운영 난이도"가 급격히 내려간다 + - 즉: 6번(ZeroMQ) vs 2번(AMQP)을 대조하려고 넣은 예제 + +### Implementing the hashsum cracker using Redis Streams (8번) + +- 의도 + - "Stream으로도 task distribution이 되긴 한다" 보여주기 +- 어떻게? + - consumer group + - pending entries + - ack +- 하지만... + - priority 없음 + - delay 없음 + - routing 없음 + - retry 설계 직접 필요 +- 핵심 메시지 + - Streams로 작업 큐를 만들 수는 있지만 + - 본래 용도는 아니다 + - Kafka Task Queue 이야기랑 정확히 같은 맥락임. + +### Implementing the Return Address pattern in AMQP (5번) + +- Return Address 패턴이란? + - 비동기 Request / Reply + - 요청 보낼 때: + - reply-to 큐 이름 + - correlation id 포함 + - 응답은 그 큐로 돌아옴 +- 언제 쓰나? + - HTTP처럼 동기 호출 ❌ + - 오래 걸리는 작업 + - 결과는 꼭 받아야 할 때 +- 왜 Broker가 필요? + - 요청자/응답자 직접 연결 ❌ + - 느슨한 결합 + - 장애 시 재처리 가능 +- 핵심 메시지 + - Broker를 쓰면 + - Request / Reply도 비동기로 풀 수 있다