EDA 기초: 도메인 이벤트, 명령 및 메시지 버스
전자상거래 시스템을 상상해 보세요. 고객이 주문을 완료하면 주문이 완료되어야 합니다. 동시에 많은 일이 발생합니다. 재고를 줄여야 하고, 이메일 알림을 보내야 하며, 주문 처리 팀에 알려야 하며 충성도 시스템을 업데이트해야 합니다. 예 그들은 이러한 서비스를 조정합니까? 고전적인 접근 방식은 Orders 서비스에서 다음을 호출하는 것입니다. 다른 모든 서비스 직접: 긴밀한 결합, 취약성, 불가능 독립적으로 등반합니다.
L'이벤트 중심 아키텍처(EDA) 이 패러다임을 뒤집는 서비스: 서비스
Orders에서 이벤트를 게시합니다. OrderPlaced 메시지 버스 및 모든 서비스
이해관계자가 독립적으로 소비합니다. Orders 서비스는 누가 주문을 듣는지 알지 못합니다.
답변을 기다립니다. 답변의 가용성에 의존하지 않습니다. 이러한 디커플링과
확장 가능하고 탄력적인 분산 시스템이 구축되는 기본 원칙입니다.
무엇을 배울 것인가
- EDA의 도메인 이벤트, 명령 및 쿼리의 차이점
- 게시-구독 패턴: 분리된 생산자와 소비자
- 메시지 버스, 이벤트 버스 및 메시지 큐: 언제 무엇을 사용할 것인가
- 이벤트 스키마: 구조, 버전 관리 및 표준 CloudEvents
- EDA와 동기식 REST의 장점과 장단점
- TypeScript로 간단한 EDA 시스템 구현
- EDA를 사용할 시기와 사용하지 않을 시기를 결정하는 방법
EDA의 세 가지 유형의 메시지
이벤트 중심 시스템의 모든 메시지가 동일한 것은 아닙니다. 차이점을 이해하세요 이벤트, 명령 및 쿼리 사이에서 올바른 EDA 시스템을 설계하기 위한 첫 번째 단계:
| 유형 | 설명 | 방향 | 답변 | Esempio |
|---|---|---|---|---|
| 도메인 이벤트 | 도메인에서 일어난 일 | 1 → N(방송) | No | OrderPlaced, PaymentReceived |
| 명령 | 작업 수행 요청 | 1 → 1(점대점) | 선택사항(비동기 ACK) | PlaceOrder, SendEmail |
| 쿼리 | 데이터에 대한 단일 요청(비동기 EDA) | 1 → 1(답글 포함) | 예(응답 대기열) | GetOrderStatus 응답 대기열을 통해 |
도메인 이벤트: EDA의 핵심
Un 도메인 이벤트 비즈니스 영역에서 이미 발생한 일을 설명합니다. 주요 속성:
- 불변: 과거를 기술하며 출판 후에도 변하지 않습니다.
- 과거에 명명된 이름:
OrderPlaced, 아니다PlaceOrder - 독립형: 소비자에게 필요한 모든 데이터를 담고 있습니다.
- 입력: 각 이벤트 유형에는 정의된 패턴이 있습니다.
// TypeScript: struttura di un Domain Event
interface DomainEvent {
eventId: string; // ID unico dell'evento (UUID)
eventType: string; // nome del tipo evento
occurredAt: string; // timestamp ISO 8601 (immutabile)
aggregateId: string; // ID dell'aggregato che ha generato l'evento
aggregateType: string; // tipo dell'aggregato (es. "Order")
version: number; // versione dello schema evento (per evoluzione)
payload: unknown; // dati specifici dell'evento
metadata?: {
correlationId?: string; // ID per tracciare la catena di eventi
causationId?: string; // ID del messaggio che ha causato questo evento
userId?: string; // utente che ha innescato l'azione
};
}
// Evento concreto: OrderPlaced
interface OrderPlacedEvent extends DomainEvent {
eventType: 'OrderPlaced';
aggregateType: 'Order';
payload: {
orderId: string;
customerId: string;
items: Array<{
productId: string;
quantity: number;
unitPrice: number;
}>;
totalAmount: number;
currency: string;
shippingAddress: {
street: string;
city: string;
country: string;
};
};
}
// Creare un OrderPlaced event
function createOrderPlacedEvent(order: Order): OrderPlacedEvent {
return {
eventId: crypto.randomUUID(),
eventType: 'OrderPlaced',
occurredAt: new Date().toISOString(),
aggregateId: order.id,
aggregateType: 'Order',
version: 1,
payload: {
orderId: order.id,
customerId: order.customerId,
items: order.items,
totalAmount: order.totalAmount,
currency: order.currency,
shippingAddress: order.shippingAddress,
},
metadata: {
correlationId: crypto.randomUUID(),
},
};
}
게시-구독 패턴
패턴 게시-구독 EDA의 기초: 출판사 (생산자)는 누가 수신하는지 알지 못한 채 이벤트를 메시지 버스로 보냅니다. 구독자 (소비자)는 누가 게시하는지 알지 못한 채 특정 유형의 이벤트를 수신하기 위해 등록합니다.
// Implementazione semplice di un Event Bus in memoria (per test/sviluppo)
type EventHandler<T extends DomainEvent> = (event: T) => Promise<void>;
class InMemoryEventBus {
private handlers = new Map<string, EventHandler<DomainEvent>[]>();
subscribe<T extends DomainEvent>(eventType: string, handler: EventHandler<T>): void {
const existing = this.handlers.get(eventType) ?? [];
this.handlers.set(eventType, [...existing, handler as EventHandler<DomainEvent>]);
}
async publish(event: DomainEvent): Promise<void> {
const eventHandlers = this.handlers.get(event.eventType) ?? [];
// Pubblica in parallelo a tutti i subscriber
await Promise.allSettled(
eventHandlers.map((handler) => handler(event))
);
}
async publishAll(events: DomainEvent[]): Promise<void> {
for (const event of events) {
await this.publish(event);
}
}
}
// Utilizzo:
const eventBus = new InMemoryEventBus();
// Inventory Service si registra per OrderPlaced
eventBus.subscribe<OrderPlacedEvent>('OrderPlaced', async (event) => {
console.log(`Decrementing inventory for order ${event.payload.orderId}`);
for (const item of event.payload.items) {
await inventoryService.decrement(item.productId, item.quantity);
}
});
// Email Service si registra per OrderPlaced
eventBus.subscribe<OrderPlacedEvent>('OrderPlaced', async (event) => {
await emailService.sendOrderConfirmation(
event.payload.customerId,
event.payload.orderId
);
});
// Order Service pubblica l'evento (non conosce i subscriber)
await eventBus.publish(createOrderPlacedEvent(placedOrder));
메시지 버스, 이벤트 버스 및 메시지 큐: 차이점
용어는 종종 같은 의미로 사용되지만 구체적인 의미는 다음과 같습니다.
- 메시지 대기열: 지점 간 대기열. 메시지가 전달됩니다. 단 하나 소비자. 예: SQS 표준 대기열
- 이벤트 버스: 방송 대상 모든 사람 구독자. 각 구독자는 이벤트 사본을 받습니다. 예: AWS EventBridge, SNS 주제
- 메시지 버스: 대기열과 주제를 모두 포함하는 일반적인 용어입니다. 실제로: 메시지 라우팅을 관리하는 브로커(RabbitMQ, Kafka)
// Esempio: stessa logica su AWS SQS + SNS (architettura fan-out comune)
// Pattern fan-out: SNS Topic + SQS Queue per ogni consumer
// 1. Pubblica su SNS Topic
// 2. SNS consegna a tutte le SQS Queue sottoscritte
// 3. Ogni servizio legge dalla propria SQS Queue indipendentemente
// Terraform per il fan-out pattern:
resource "aws_sns_topic" "order_events" {
name = "order-events"
}
resource "aws_sqs_queue" "inventory_queue" {
name = "inventory-order-events"
}
resource "aws_sqs_queue" "email_queue" {
name = "email-order-events"
}
resource "aws_sns_topic_subscription" "inventory" {
topic_arn = aws_sns_topic.order_events.arn
protocol = "sqs"
endpoint = aws_sqs_queue.inventory_queue.arn
}
resource "aws_sns_topic_subscription" "email" {
topic_arn = aws_sns_topic.order_events.arn
protocol = "sqs"
endpoint = aws_sqs_queue.email_queue.arn
}
CloudEvents: 이벤트 스키마의 표준
클라우드이벤트 및 구조를 표준화하는 CNCF 사양 서로 다른 시스템 간의 이벤트. 이를 채택하면 상호 운용성이 촉진되고 도구가 단순화됩니다. 모니터링 및 디버깅:
// CloudEvents v1.0 - struttura standard
{
"specversion": "1.0",
"id": "550e8400-e29b-41d4-a716-446655440000",
"type": "com.company.order.placed", // Reverse DNS + evento
"source": "/orders-service/v1", // URI del servizio sorgente
"subject": "order-789", // identificativo della risorsa
"time": "2026-03-20T10:30:00Z", // timestamp ISO 8601
"datacontenttype": "application/json",
"dataschema": "https://schemas.company.com/order/placed/v1.json",
"data": {
"orderId": "order-789",
"customerId": "cust-123",
"totalAmount": 150.00,
"currency": "EUR"
}
}
// TypeScript: creare un CloudEvent con la SDK ufficiale
import { CloudEvent } from "cloudevents";
const event = new CloudEvent({
specversion: "1.0",
type: "com.company.order.placed",
source: "/orders-service/v1",
subject: `order-${orderId}`,
datacontenttype: "application/json",
dataschema: "https://schemas.company.com/order/placed/v1.json",
data: {
orderId: order.id,
customerId: order.customerId,
totalAmount: order.totalAmount,
currency: order.currency,
},
});
// Valida il CloudEvent prima di pubblicarlo
if (!event.source || !event.type) {
throw new Error("CloudEvent validation failed: missing required fields");
}
이벤트 버전 관리
이벤트는 여러 서비스에서 독립적으로 사용됩니다. 패턴 변경 버전 관리 전략이 없는 이벤트는 소비자를 혼란스럽게 합니다. 주요 패턴:
// Pattern 1: Versioning nel tipo evento
// Vecchi consumer continuano a ricevere v1, nuovi consumer si registrano per v2
eventBus.subscribe('OrderPlaced.v1', handleOrderPlacedV1);
eventBus.subscribe('OrderPlaced.v2', handleOrderPlacedV2);
// Pattern 2: Backward-compatible changes (aggiunta di campi opzionali)
// SAFE: aggiungere nuovi campi opzionali (consumer ignorano i campi sconosciuti)
interface OrderPlacedEventV1 {
orderId: string;
customerId: string;
totalAmount: number;
}
interface OrderPlacedEventV2 extends OrderPlacedEventV1 {
// Aggiunto in V2: opzionale, backward-compatible
estimatedDeliveryDate?: string;
loyaltyPointsEarned?: number;
}
// Pattern 3: Parallel publishing (per breaking changes)
// Pubblica sia v1 che v2 per un periodo di transizione
async function publishOrderPlaced(order: Order): Promise<void> {
const v1Event = createOrderPlacedV1(order);
const v2Event = createOrderPlacedV2(order);
await Promise.all([
eventBus.publish(v1Event), // per consumer legacy
eventBus.publish(v2Event), // per consumer aggiornati
]);
}
// NEVER: rimuovere campi, cambiare tipi, rinominare campi obbligatori
// -> breaking change: migra prima tutti i consumer poi rimuovi v1
EDA의 이점과 장단점
EDA를 사용해야 하는 경우
- 필요한 분리: 게시자를 변경하지 않고 새로운 소비자를 추가하려는 경우
- 독립적인 확장성: 서로 다른 부하를 가진 다양한 소비자가 개별적으로 확장됨
- 감사 추적: 불변 이벤트는 시스템에서 발생한 모든 일의 자연 로그입니다.
- 실패에 대한 회복력: 소비자가 다운되면 메시지 버스는 다시 작동할 때까지 메시지를 보관합니다.
- 시스템 간 통합: 표준 이벤트를 통해 통신하는 이기종 시스템
EDA를 사용하지 말아야 할 경우
- 즉각적인 대응이 필요합니다: 사용자가 동기 결과를 기다려야 하는 경우 EDA는 불필요한 지연 시간과 복잡성을 추가합니다.
- 간단한 시스템: 기능이 거의 없는 단일체는 메시지 브로커의 오버헤드로부터 이점을 얻지 못합니다.
- 단순 분산 트랜잭션: 여러 서비스에서 원자성이어야 하는 작업의 경우 EDA에는 Saga 패턴이 필요합니다(복잡도 높음).
- EDA 경험이 없는 소규모 팀: 학습 곡선은 중요합니다. REST로 시작하고 필요한 곳에 EDA를 추가하세요.
전체 흐름: 전자상거래 예시
// Flusso completo EDA per un ordine e-commerce
// 1. Order Service: riceve HTTP POST /orders
// 2. Valida, persiste, pubblica evento
class OrderService {
constructor(
private readonly orderRepo: OrderRepository,
private readonly eventBus: EventBus
) {}
async placeOrder(dto: PlaceOrderDto): Promise<Order> {
// Logica business: crea l'ordine
const order = Order.create(dto);
// Persisti nel database
await this.orderRepo.save(order);
// Pubblica gli eventi generati dall'aggregato
const events = order.getUncommittedEvents();
await this.eventBus.publishAll(events);
order.clearEvents();
return order;
}
}
// 3. Inventory Service: ascolta OrderPlaced
// - Scala indipendentemente con 5 consumer paralleli
// - Se giu, i messaggi si accumulano nella queue
// 4. Email Service: ascolta OrderPlaced
// - Invia email di conferma
// - Se fallisce, il messaggio va in DLQ per retry
// 5. Loyalty Service: ascolta OrderPlaced
// - Calcola e aggiunge punti fedeltà
// - Pubblica LoyaltyPointsEarned
// 6. Analytics Service: ascolta OrderPlaced + LoyaltyPointsEarned
// - Aggiorna le metriche in tempo reale
// Il servizio Order non sa niente di tutto questo!
// Aggiungere un nuovo consumer = zero modifiche al publisher
결론 및 다음 단계
EDA는 패러다임 전환입니다. 서비스가 서로 "호출"하는 시스템에서 서비스가 "이벤트를 통해 통신"하는 시스템입니다. 디커플링 이득, 확장성과 탄력성은 현실적이지만 관리라는 새로운 과제에 직면해야 합니다. 오류가 비동기화되고 디버깅에는 상관 ID와 분산 추적이 필요합니다. 일관성은 "최종"이 되어야 합니다.
이 시리즈의 다음 기사에서는 EDA를 만드는 고급 패턴을 다룹니다. 프로덕션에서 실행 가능: 불변 상태를 위한 이벤트 소싱, 분리를 위한 CQRS 읽기/쓰기, 분산 트랜잭션을 위한 Saga, AWS 도구(EventBridge, SQS, SNS) 클라우드 환경에서 구현하는 것입니다.
이벤트 중심 아키텍처 시리즈의 향후 기사
관련 시리즈
- Apache Kafka 및 스트림 처리 — 대용량 EDA 시스템을 위한 백본으로서의 Kafka
- 규모에 맞는 Kubernetes — Kubernetes에서 EDA 마이크로서비스를 조정합니다.
- 실용적인 소프트웨어 아키텍처 — EDA와 REST, 모놀리스와 마이크로서비스의 경우







