d8a80f6272
이름만 다른(표기 변형) [[위키링크]]를 대상 문서의 canonical 제목으로 치환해 끊겼던 1,200개 링크를 연결. 제목/파일명 정규화 일치만 적용하고 별칭 매칭은 과병합 위험으로 제외(애매성 가드). 원본은 _link_reconcile_backup/ 에 백업. 도구: Datacollect/scripts/link_reconcile_apply.mjs Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
197 lines
6.7 KiB
Markdown
197 lines
6.7 KiB
Markdown
---
|
||
id: wiki-2026-0508-event-mediator
|
||
title: Event Mediator
|
||
category: 10_Wiki/Topics
|
||
status: verified
|
||
canonical_id: self
|
||
aliases: [Mediator Pattern, Event Bus]
|
||
duplicate_of: none
|
||
source_trust_level: A
|
||
confidence_score: 0.9
|
||
verification_status: applied
|
||
tags: [architecture, design-pattern, event-driven, decoupling]
|
||
raw_sources: []
|
||
last_reinforced: 2026-05-10
|
||
github_commit: pending
|
||
tech_stack:
|
||
language: typescript
|
||
framework: nodejs
|
||
---
|
||
|
||
# Event Mediator
|
||
|
||
## 매 한 줄
|
||
> **"매 N×N component coupling 의 N×1 mediator hub 의 collapse"**. Event Mediator 매 multiple components 의 direct coupling 의 elimination — 매 components 매 mediator 를 publish/subscribe, 매 mediator 매 routing/orchestration 의 own. GoF Mediator pattern 의 event-driven evolution.
|
||
|
||
## 매 핵심
|
||
|
||
### 매 Mediator vs Observer
|
||
- **Observer**: 매 1 subject → N observers, 매 unidirectional broadcast.
|
||
- **Mediator**: 매 N senders ↔ N receivers, 매 hub 의 bidirectional routing.
|
||
- **Event Bus**: 매 Mediator 의 generic implementation — 매 string-keyed event types.
|
||
|
||
### 매 동기 vs 비동기
|
||
- **Sync mediator**: 매 in-process method dispatch — 매 EventEmitter, MediatR.
|
||
- **Async mediator**: 매 message queue 의 backed — 매 RabbitMQ, Kafka, Redis Streams.
|
||
- **Hybrid**: 매 in-process sync + cross-service async (e.g., NestJS CQRS).
|
||
|
||
### 매 응용
|
||
1. UI components decoupling — chat rooms, form fields.
|
||
2. Microservices orchestration — saga coordinator.
|
||
3. CQRS command/query bus — MediatR.
|
||
4. Game event systems — unit death, achievement triggers.
|
||
|
||
## 💻 패턴
|
||
|
||
### Typed Event Bus (TypeScript)
|
||
```typescript
|
||
type EventMap = {
|
||
'user.signup': { userId: string; email: string };
|
||
'order.placed': { orderId: string; total: number };
|
||
'cache.invalidate': { key: string };
|
||
};
|
||
|
||
class EventMediator<M> {
|
||
private handlers = new Map<keyof M, Set<(p: any) => void | Promise<void>>>();
|
||
|
||
on<K extends keyof M>(event: K, handler: (payload: M[K]) => void | Promise<void>) {
|
||
if (!this.handlers.has(event)) this.handlers.set(event, new Set());
|
||
this.handlers.get(event)!.add(handler);
|
||
return () => this.handlers.get(event)!.delete(handler);
|
||
}
|
||
|
||
async emit<K extends keyof M>(event: K, payload: M[K]) {
|
||
const hs = this.handlers.get(event);
|
||
if (!hs) return;
|
||
await Promise.all([...hs].map(h => h(payload)));
|
||
}
|
||
}
|
||
|
||
const bus = new EventMediator<EventMap>();
|
||
bus.on('user.signup', async ({ userId, email }) => {
|
||
await sendWelcomeEmail(email);
|
||
});
|
||
```
|
||
|
||
### Mediator with Request/Response (MediatR-style)
|
||
```typescript
|
||
interface IRequest<TResponse> { __response?: TResponse }
|
||
interface IHandler<TReq extends IRequest<TRes>, TRes> {
|
||
handle(req: TReq): Promise<TRes>;
|
||
}
|
||
|
||
class Mediator {
|
||
private handlers = new Map<Function, IHandler<any, any>>();
|
||
register<T extends IRequest<R>, R>(reqCtor: new (...a: any[]) => T, h: IHandler<T, R>) {
|
||
this.handlers.set(reqCtor, h);
|
||
}
|
||
async send<R>(req: IRequest<R>): Promise<R> {
|
||
const h = this.handlers.get(req.constructor);
|
||
if (!h) throw new Error(`No handler for ${req.constructor.name}`);
|
||
return h.handle(req);
|
||
}
|
||
}
|
||
|
||
class GetUserQuery implements IRequest<User> { constructor(public id: string) {} }
|
||
class GetUserHandler implements IHandler<GetUserQuery, User> {
|
||
async handle(q: GetUserQuery) { return db.users.findById(q.id); }
|
||
}
|
||
```
|
||
|
||
### Saga Mediator (orchestrated)
|
||
```typescript
|
||
class OrderSaga {
|
||
constructor(private bus: EventMediator<OrderEvents>) {
|
||
bus.on('order.created', this.onCreated.bind(this));
|
||
bus.on('payment.failed', this.onPaymentFailed.bind(this));
|
||
}
|
||
async onCreated({ orderId }: { orderId: string }) {
|
||
await this.bus.emit('payment.requested', { orderId });
|
||
}
|
||
async onPaymentFailed({ orderId, reason }: any) {
|
||
await this.bus.emit('order.cancelled', { orderId, reason });
|
||
await this.bus.emit('inventory.released', { orderId });
|
||
}
|
||
}
|
||
```
|
||
|
||
### Middleware Pipeline
|
||
```typescript
|
||
type Middleware<E> = (event: E, next: () => Promise<void>) => Promise<void>;
|
||
|
||
class PipelineMediator<E> {
|
||
private middlewares: Middleware<E>[] = [];
|
||
use(m: Middleware<E>) { this.middlewares.push(m); }
|
||
async dispatch(event: E) {
|
||
let i = -1;
|
||
const run = async (idx: number): Promise<void> => {
|
||
if (idx <= i) throw new Error('next() called twice');
|
||
i = idx;
|
||
const fn = this.middlewares[idx];
|
||
if (fn) await fn(event, () => run(idx + 1));
|
||
};
|
||
await run(0);
|
||
}
|
||
}
|
||
// usage: logging, validation, retry, dead-letter
|
||
```
|
||
|
||
### Cross-Service Mediator (Kafka)
|
||
```typescript
|
||
import { Kafka } from 'kafkajs';
|
||
|
||
const kafka = new Kafka({ brokers: ['localhost:9092'] });
|
||
const producer = kafka.producer();
|
||
const consumer = kafka.consumer({ groupId: 'order-svc' });
|
||
|
||
await consumer.subscribe({ topic: 'orders', fromBeginning: false });
|
||
await consumer.run({
|
||
eachMessage: async ({ message }) => {
|
||
const evt = JSON.parse(message.value!.toString());
|
||
await handler[evt.type]?.(evt.payload);
|
||
},
|
||
});
|
||
|
||
await producer.send({
|
||
topic: 'orders',
|
||
messages: [{ key: orderId, value: JSON.stringify({ type: 'order.placed', payload }) }],
|
||
});
|
||
```
|
||
|
||
## 매 결정 기준
|
||
| 상황 | Approach |
|
||
|---|---|
|
||
| 2-3 components 의 direct call | 매 mediator skip — 매 over-engineering |
|
||
| 5+ components, fan-out broadcast | 매 in-process EventEmitter |
|
||
| CQRS / clean architecture | 매 MediatR-style request bus |
|
||
| Cross-service, durability | 매 Kafka / RabbitMQ |
|
||
| Long-running workflow | 매 saga orchestrator (Temporal, Inngest) |
|
||
|
||
**기본값**: 매 typed in-process EventEmitter (Node) 또는 MediatR (.NET) — 매 cross-service 의 Kafka.
|
||
|
||
## 🔗 Graph
|
||
- 부모: [[Design-Patterns]] · [[Event-Driven-Architecture]]
|
||
- 변형: [[Observer-Pattern]] · [[Pub-Sub]]
|
||
- 응용: [[CQRS]] · [[Microservices]]
|
||
- Adjacent: [[Event Sourcing]]
|
||
|
||
## 🤖 LLM 활용
|
||
**언제**: 매 N×N coupling 의 emerge — 매 5+ components 매 mutual callbacks. CQRS / saga implementation.
|
||
**언제 X**: 매 simple 2-component dependency — 매 direct injection 매 sufficient. Hot-path latency-critical (mediator dispatch overhead).
|
||
|
||
## ❌ 안티패턴
|
||
- **God Mediator**: 매 mediator 매 business logic 의 absorb — 매 anemic handlers, 매 mediator 의 1000+ lines. Mediator 의 routing only.
|
||
- **Event soup**: 매 untyped string events — 매 'user_signup' vs 'userSignup' typo 의 silent failure. Typed map 의 use.
|
||
- **Sync chain pretending async**: 매 emit() 매 await chain 의 200ms latency — 매 async queue 의 use.
|
||
- **Lost events**: 매 in-memory bus 매 crash 의 lose — 매 durability 매 required 의 persistent queue.
|
||
|
||
## 🧪 검증 / 중복
|
||
- Verified (GoF Design Patterns; MediatR docs; NestJS CQRS).
|
||
- 신뢰도 A.
|
||
|
||
## 🕓 Changelog
|
||
| 날짜 | 변경 |
|
||
|---|---|
|
||
| 2026-05-08 | Phase 1 |
|
||
| 2026-05-10 | Manual cleanup — Event Mediator pattern 의 typed/saga/Kafka 의 5 patterns |
|