EDA (Event-Driven Architecture)
분류: Layer 6 - 운영 심화: 관측성 & 복원력 | 작성일: 2026-03-22 | 선수지식: Queue/Worker Basics
1. 한 줄 정의
섹션 제목: “1. 한 줄 정의”EDA(Event-Driven Architecture)는 시스템 구성요소들이 직접 호출하는 대신 “이벤트”를 발행(Publish)하고 구독(Subscribe)하는 방식으로 통신하는 아키텍처 패턴이다.
2. 왜 중요한가
섹션 제목: “2. 왜 중요한가”프론트엔드 브릿지: React의
useEffect는 상태 변화 이벤트에 반응한다. Redux의 미들웨어는 액션이 발생할 때 부수 효과를 처리한다. EDA는 이 패턴을 서비스 간 레벨로 확장한 것이다. “어떤 서비스에서 이벤트가 발생하면 → 다른 서비스들이 각자의 방식으로 반응한다”는 구조가 동일하다. DOM의element.addEventListener('click', handler)가NestJS EventEmitter2의@OnEvent('order.created')가 되고, 이것이 다시 SQS Consumer로 확장된다. 규모만 다를 뿐 반응형 패턴은 같다.
마이크로서비스 환경에서 서비스 간 직접 API 호출은 의존성과 장애 전파를 만든다. EDA는 이 문제를 해결하는 핵심 패턴이다. Nest.js에도 내장 EventEmitter와 외부 메시지 브로커 연동 모듈이 있고, 실무에서 서비스 간 통신 구조를 설계할 때 반드시 마주치는 개념이다.
2.5. 선행 구조의 한계 — 직접 호출에서 이벤트로
섹션 제목: “2.5. 선행 구조의 한계 — 직접 호출에서 이벤트로”EDA는 “새로운 통신 방식”이라기보다, 마이크로서비스에서 직접 호출이 만든 강결합을 줄이기 위해 등장한 구조다. 예를 들어 OrderService가 주문 생성 중 PaymentService → InventoryService → NotificationService를 순서대로 동기 호출하면, 세 하위 서비스가 각각 99.9% 성공하더라도 전체 성공률은 0.999 × 0.999 × 0.999 = 99.7003%가 된다(단순 독립 실패 가정). 여기에 이메일 발송 지연이나 외부 결제사의 p95 지연이 섞이면 주문 API의 응답 시간도 함께 늘어난다. 프론트엔드에서 클릭 핸들러가 분석 로그·알림·서버 저장까지 모두 기다리면 UI가 멈추는 것과 같은 원리다.
브로커 기반 EDA는 이 한계를 “결정 경로”와 “후속 반응”을 분리해서 푼다. 주문 생성의 핵심 트랜잭션은 DB 커밋까지로 제한하고, 결제 후 알림·감사 로그·추천 모델 업데이트처럼 응답에 즉시 필요하지 않은 작업은 order.created 이벤트로 발행한다. AWS 공식 결정 가이드도 SQS를 마이크로서비스 분리·요청 버퍼링·비동기 작업 처리에, SNS를 팬아웃 알림에, EventBridge를 규칙 기반 라우팅과 크로스 계정 이벤트 공유에 배치한다. 즉 이 문서의 lineage_oneliner처럼, EDA의 핵심은 마이크로서비스의 강결합을 브로커 기반 비동기 이벤트 통신으로 바꾸는 것이다.
3. 핵심 개념
섹션 제목: “3. 핵심 개념”EDA가 동작하는 방식 (전체 흐름)
섹션 제목: “EDA가 동작하는 방식 (전체 흐름)”비유: 식당 주문 시스템과 같다. 손님(Publisher)이 주문서를 작성해서 주방 창구(Event Broker)에 꽂으면, 조리팀·음료팀·서빙팀(Subscriber들)이 각자 관련 주문을 가져가서 처리한다. 손님은 어떤 팀이 처리하는지 몰라도 되고, 각 팀도 손님이 누구인지 알 필요 없다.
원리: EDA의 3가지 핵심 구성요소
- Publisher(생산자): 이벤트를 발행하고 즉시 다음 작업을 진행. 누가 구독하는지 신경 쓰지 않음.
- Event Broker(중계자): 이벤트를 받아 저장하고, 구독자에게 전달. AWS SNS, SQS, Kafka, RabbitMQ가 이 역할.
- Subscriber(소비자): 관심 있는 이벤트만 구독하고 처리. 발행자가 누구인지 몰라도 됨.
브로커 내부 동작:
Publisher → [Broker] → 구독자 목록 조회 → 각 Subscriber에게 전달 ↓ 이벤트 저장 (at-least-once 보장) 필터링 (어떤 구독자에게 보낼지 결정) 재시도 (전달 실패 시 재전송)왜 이렇게 설계되었는가 — 브로커가 필요한 이유
Publisher가 Subscriber를 직접 호출하면 두 가지 문제가 생긴다. 첫째, Publisher가 모든 Subscriber의 주소(URL, 큐 이름)를 알아야 한다 — 결합도 증가. 둘째, Subscriber가 다운됐을 때 이벤트가 유실된다. 브로커는 이 두 문제를 해결한다: Publisher는 브로커에만 발행하면 되고(위치 투명성), 브로커가 Subscriber가 복구될 때까지 이벤트를 보관한다(내구성). AWS SNS+SQS 공식 문서의 표현으로는 SNS가 시간 민감한 메시지를 여러 구독자에게 push하고, SQS는 수신 컴포넌트가 동시에 살아 있지 않아도 나중에 처리할 수 있도록 메시지를 보존한다. 이 조합이 “한 이벤트에 여러 반응”과 “느린 소비자 격리”를 동시에 만족시키므로, 주문 생성 후 이메일·푸시·감사 로그처럼 응답 경로 밖의 작업에 적합하다.
출처: https://docs.aws.amazon.com/sns/latest/dg/sns-sqs-as-subscriber.html
2025년 EventBridge 신기능 — 더욱 강력해진 지능형 라우팅
2025년 Amazon EventBridge는 향상된 로깅 기능을 추가했다. 이제 이벤트 전체 라이프사이클(성공/실패/재시도 지표 포함)을 상세히 추적할 수 있다. 또한 Cross-Account 직접 전달이 가능해져, 다른 AWS 계정의 SQS Queue로 직접 이벤트를 전송할 수 있다 (기존에는 중간 인프라가 필요했음).
기존 방식 (크로스 계정):계정 A EventBridge → 계정 A Lambda → 계정 B SQS
2025년 방식 (직접 전달):계정 A EventBridge → 계정 B SQS (직접!)→ 멀티 계정 환경(개발/스테이징/프로덕션 계정 분리)에서 아키텍처가 크게 단순해짐출처: https://aws.amazon.com/about-aws/whats-new/2025/01/amazon-eventbridge-direct-delivery-cross-account-targets/, https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-service-cross-account.html
📖 더 보기: Publisher-Subscriber Pattern - Microsoft Azure 아키텍처 가이드 — 위 브로커 내부 흐름(필터링, 재시도, 보장 수준)을 다이어그램으로 확인 가능
EDA의 핵심: 느슨한 결합
# 직접 호출 (강한 결합)OrderService.createOrder() → PaymentService.charge() # PaymentService가 느리면 전체 대기 → NotificationService.send() # 하나가 죽으면 전체 실패
# EDA (느슨한 결합)OrderService → [order.created 이벤트 발행] → 즉시 응답 반환 ↓ (비동기, 순서 무관) PaymentService 구독 → 결제 처리 (독립 실행) NotificationService 구독 → 알림 발송 (독립 실행) InventoryService 구독 → 재고 차감 (독립 실행)Publisher / Subscriber / Event Broker
- Publisher: 이벤트를 발행하는 쪽. 누가 구독하는지 모름.
- Subscriber: 이벤트를 받아 처리하는 쪽. 누가 발행했는지 모름.
- Event Broker: 이벤트를 중계하는 인프라 (Kafka, RabbitMQ, AWS SNS/SQS)
Queue vs Pub/Sub 차이
| Queue (1:1) | Pub/Sub (1:N) | |
|---|---|---|
| 소비자 | 하나의 Consumer가 처리 | 여러 Subscriber가 각자 처리 |
| 예시 | SQS → Worker 하나가 처리 | SNS → 여러 Lambda/SQS가 동시 처리 |
| 사용 | 작업 분산, 비동기 처리 | 이벤트 브로드캐스트 |
| 중복처리 | 동일 메시지를 한 번만 처리 | 각 구독자가 같은 메시지를 모두 처리 |
AWS에서의 SNS + SQS Fan-out 패턴 (실무에서 가장 많이 쓰이는 구조):
OrderService │ ▼ Publish[SNS Topic: order-events] │ ┌──┴──┬──────────────┐ ▼ ▼ ▼[SQS] [SQS] [Lambda]payment inventory notificationworker worker function
# SNS가 이벤트를 받아 여러 SQS/Lambda에 팬아웃# 각 SQS에는 Worker가 붙어서 독립적으로 처리Nest.js에서의 EDA
1) 내장 EventEmitter (프로세스 내 이벤트)
// 설치: npm install @nestjs/event-emitter eventemitter2import { EventEmitterModule } from "@nestjs/event-emitter";
@Module({ imports: [EventEmitterModule.forRoot()],})export class AppModule {}
// order.service.ts — 이벤트 발행import { EventEmitter2 } from "@nestjs/event-emitter";
@Injectable()export class OrderService { constructor(private eventEmitter: EventEmitter2) {}
async createOrder(dto: CreateOrderDto) { const order = await this.orderRepository.save(dto);
// 이벤트 발행 — 누가 처리하는지 신경 쓰지 않음 this.eventEmitter.emit("order.created", { orderId: order.id, userId: order.userId, });
return order; // 즉시 응답 반환 (비동기 처리) }}
// notification.service.ts — 이벤트 구독import { OnEvent } from "@nestjs/event-emitter";
@Injectable()export class NotificationService { @OnEvent("order.created") async handleOrderCreated(payload: { orderId: string; userId: string }) { console.log(`주문 생성 이벤트 수신: ${payload.orderId}`); await this.sendEmail(payload.userId, "주문이 완료되었습니다."); }}예상 실행 흐름:
POST /orders 요청 → OrderService.createOrder() 실행 → order.created 이벤트 emit → 즉시 { id: 'order-123', ... } 응답 반환 → (비동기) NotificationService.handleOrderCreated() 실행 → 이메일 발송2) 외부 메시지 브로커 연동 (서비스 간 이벤트)
// AWS SQS와 연동하는 경우 (@ssut/nestjs-sqs 패키지 사용)// 설치: npm install @ssut/nestjs-sqs @aws-sdk/client-sqs
@SqsMessageHandler('order-created-queue', false)async handleOrderCreated(message: AWS.SQS.Message) { const payload = JSON.parse(message.Body); console.log('SQS 메시지 수신:', payload); // { orderId: 'order-123', userId: 'user-456', timestamp: '...' } await this.processOrder(payload);}📖 더 보기: Event-Driven Architecture with NestJS: Using the EventEmitter Module — 위 @OnEvent 패턴의 전체 예제와 에러 처리 방법 확인 가능
EDA 심화 패턴: Outbox Pattern (이벤트 유실 방지)
EDA에서 자주 발생하는 문제: DB에 주문을 저장했지만 이벤트 발행이 실패하면 데이터 불일치 발생.
문제 상황:1. DB에 ORDER 저장 (성공)2. SNS/SQS에 order.created 이벤트 발행 (실패!)→ DB에는 주문이 있는데, 결제/알림 서비스에는 이벤트가 전달 안 됨
해결: Outbox Pattern1. DB 트랜잭션 안에서 ORDER 저장 + outbox 테이블에 이벤트 레코드도 함께 저장2. 별도 프로세스(Outbox Processor)가 outbox 테이블을 폴링해서 이벤트 발행3. 발행 성공 시 outbox 레코드를 처리 완료로 표시→ DB 저장과 이벤트 발행이 원자적으로 묶임 (유실 불가)📌 Outbox Pattern 상세 구현(outbox-relay.service.ts, cron relay, idempotency key)은 L9 cqrs-event-sourcing.md에서 다룹니다. L6에서는 “DB 저장과 이벤트 발행을 원자적으로 묶어야 유실이 없다”는 개념 이해로 충분합니다.
📖 더 보기: Outbox & Saga Pattern on AWS EDA — Outbox 패턴과 Saga 패턴이 실제 AWS 환경에서 어떻게 연결되는지 다이어그램 포함 설명 (중급)
이벤트 소싱 (Event Sourcing) — EDA의 다음 단계
EDA에서 이벤트를 발행·소비하는 것에 익숙해지면, “이벤트 자체를 DB에 저장해 상태를 이벤트 재생으로 복원”하는 Event Sourcing으로 확장할 수 있다. 현재 상태(status = ‘delivered’) 대신 order.created → order.paid → order.shipped → order.delivered 이벤트 시퀀스를 저장하는 방식이다. 이벤트 자체가 감사 로그(Audit Log)가 되고, 특정 시점 상태로 되돌리는 시간여행 디버깅도 가능해진다.
단, Event Sourcing은 복잡성 비용이 상당하다. 이벤트 스키마 버전 관리(수년 전 이벤트도 올바르게 재생), Projection 지연에 따른 최종 일관성 처리, CQRS와의 결합 설계까지 추가 고려사항이 많다. 금융·의료·법률처럼 감사 이력이 필수인 도메인이 아니라면 단순 CRUD에 도입하는 것은 과도한 엔지니어링이다.
📖 Event Sourcing의 Event Store, Projection, Snapshot, Upcaster 패턴 등 상세 구현은 L9 아키텍처 패턴(cqrs-event-sourcing.md) 에서 다룬다. L6 수준에서는 “EDA → Event Sourcing으로 이어지는 개념적 연결”을 이해하는 것으로 충분하다.
실전 아키텍처 패턴
섹션 제목: “실전 아키텍처 패턴”패턴 1: AWS 브로커 선택 기준 (SNS vs SQS vs EventBridge)
실무에서 “어떤 브로커를 쓸지”는 자주 나오는 결정이다. 먼저 질문을 하나로 줄이면 쉽다: 소비자가 처리 속도를 직접 조절해야 하면 SQS, 같은 이벤트를 여러 대상에 즉시 뿌려야 하면 SNS, 이벤트 내용으로 조건 라우팅하거나 계정 경계를 넘어야 하면 EventBridge다. AWS 공식 결정 가이드 기준으로 SQS는 pull 모델과 메시지 보존, SNS는 push 기반 팬아웃, EventBridge는 규칙 매칭과 콘텐츠 기반 필터링이 핵심 차이다.
| 서비스 | 전달 방식 | 주요 사용 사례 | 선택 기준 |
|---|---|---|---|
| SQS | Pull(폴링) | 비동기 작업 큐, Worker 패턴 | 순서/속도 조절이 필요한 1:1 처리 |
| SNS | Push(즉시) | 실시간 팬아웃, 모바일 푸시, 여러 서비스 동시 알림 | 즉시 1:N 브로드캐스트 |
| EventBridge | Push(라우팅) | 복잡한 이벤트 라우팅, AWS 서비스 간 연동, 스케줄러 | 패턴 매칭, 크로스 계정, 서드파티 연동 |
권장 조합 아키텍처:EventBridge (지능형 라우터, 패턴 매칭) ↓ SNS Topic (팬아웃, 실시간 전달) ┌──┴──┬──────────────┐ ↓ ↓ ↓[SQS] [SQS] [Lambda](버퍼링, 재처리) (즉시 처리)EDA를 선택하지 말아야 할 때
EDA가 항상 정답은 아니다. 판단 기준은 “이 작업이 사용자 응답이나 원자적 결정에 필요한가?”다. 결제 승인 여부를 화면에 즉시 보여줘야 하는 주문 API라면 payment.approved 이벤트를 기다리는 구조보다 결제 API를 동기 호출하고, 성공 후 order.paid 이벤트를 발행하는 쪽이 단순하다. 반대로 이메일 발송·Slack 알림·추천 모델 갱신은 실패해도 재시도나 DLQ로 복구할 수 있으므로 이벤트로 분리하는 편이 낫다. AWS 공식 문서도 일부 워크로드는 강한 일관성 요구 때문에 EDA에 적합하지 않다고 설명한다.
| 상황 | 이유 | 대안 |
|---|---|---|
| 강한 ACID 트랜잭션 필수 | EDA는 최종 일관성(Eventual Consistency) 기반 — 분산 환경에서 ACID 보장 불가 | 관계형 DB + 동기 API |
| 소규모 모놀리스 | 브로커 운영 복잡성이 비즈니스 가치보다 크다 (오버엔지니어링) | 동일 프로세스 내 함수 호출 |
| 즉각적 UI 응답 필수 | 결제 완료 UI처럼 응답을 사용자에게 즉시 보여줘야 하는 경우 (비동기 특성과 충돌) | 동기 REST API |
| 서브밀리초 저지연 필수 | EDA는 네트워크를 통한 브로커 경유로 추가 지연 발생 — 고빈도 트레이딩·로봇 자동화 같은 워크로드에 부적합 | 동기 직접 호출 또는 in-memory |
판단 기준 — 이 질문에 모두 “예”라면 EDA 적합:
- 서비스 간 응답 대기가 불필요한가? (비동기 처리 가능)
- 처리 실패 시 재시도로 복구 가능한가? (멱등성 구현 가능)
- 하나의 이벤트에 여러 수신자가 관심을 가지는가? (1:N 구조)
📖 AWS 공식: What is EDA? - Event-Driven Architecture Explained — “일부 워크로드는 강한 일관성 요구사항으로 인해 EDA에 적합하지 않다”
패턴 1-B: EventBridge Content Filtering — 조건별 라우팅 (NestJS 예시)
EventBridge의 핵심 장점은 이벤트 내용(Content)을 기반으로 다른 타깃에 라우팅하는 패턴 매칭이다. SNS/SQS는 “모든 메시지를 전달”하지만 EventBridge는 “조건에 맞는 메시지만 특정 타깃으로 전달”한다.
// NestJS에서 EventBridge로 이벤트 발행 (AWS SDK v3)import { EventBridgeClient, PutEventsCommand,} from "@aws-sdk/client-eventbridge";
@Injectable()export class OrderEventPublisher { private readonly client = new EventBridgeClient({ region: "ap-northeast-2" });
async publishOrderCreated(order: Order): Promise<void> { await this.client.send( new PutEventsCommand({ Entries: [ { Source: "com.myapp.orders", DetailType: "OrderCreated", Detail: JSON.stringify({ orderId: order.id, userId: order.userId, amount: order.amount, tier: order.userTier, // "standard" | "premium" | "vip" }), EventBusName: process.env.EVENT_BUS_NAME, }, ], }), ); }}EventBridge 콘솔(또는 IaC)에서 규칙(Rule)을 만들어 Content Filtering 적용:
// Rule 1: VIP 주문만 프리미엄 처리 큐로{ "source": ["com.myapp.orders"], "detail-type": ["OrderCreated"], "detail": { "tier": ["vip"], "amount": [{ "numeric": [">=", 100000] }] }}// → 타깃: SQS priority-order-queue (전담 워커)
// Rule 2: 전체 주문은 일반 처리 큐로{ "source": ["com.myapp.orders"], "detail-type": ["OrderCreated"]}// → 타깃: SQS standard-order-queue이 패턴의 장점: Publisher는 이벤트 하나만 발행, 라우팅 로직은 EventBridge 규칙이 담당 → 새 처리 조건이 생겨도 Publisher 코드를 변경하지 않아도 된다.
패턴 1-C: 오픈소스 브로커 vs AWS 관리형 — Kafka / RabbitMQ / SQS 선택 기준
AWS SNS/SQS/EventBridge 외에도 Kafka, RabbitMQ는 현장에서 폭넓게 쓰인다. 세 브로커는 설계 철학이 다르므로 팀 역량과 요구사항에 맞게 골라야 한다.
| 항목 | Kafka | RabbitMQ | AWS SQS/SNS |
|---|---|---|---|
| 설계 목적 | 고처리량 이벤트 스트리밍, 로그 기반 저장 | 유연한 메시지 라우팅, 낮은 지연 | AWS 관리형 큐, 운영 부담 최소화 |
| 처리량 | 초당 수백만 건 (순차 디스크 쓰기) | 중간 수준 (Kafka보다 낮음) | FIFO 기본 batching 기준 3,000 messages/s per API method |
| 메시지 재처리 | 가능 (로그 보존으로 오프셋 리셋) | 불가 (소비 후 삭제) | 불가 (보존 기간 내 가시성 조정만) |
| 운영 복잡도 | 높음 (ZooKeeper/KRaft, 파티션 관리) | 중간 (클러스터 설정 필요) | 없음 (AWS 완전 관리형) |
| 주 사용 사례 | 데이터 파이프라인, 이벤트 소싱, 실시간 분석 | 복잡한 라우팅, 멀티 프로토콜 | AWS 네이티브 마이크로서비스, 서버리스 |
| AWS 종속성 | 없음 (온프레미스 가능) | 없음 | 있음 |
선택 가이드:
- Kafka: 이벤트 재생이 필요하거나 하루 수억 건 이상 처리 — 단, 팀에 Kafka 운영 역량 필수
- RabbitMQ: 복잡한 라우팅 규칙(교환기 타입)이나 낮은 지연이 중요하고 팀이 자체 운영 가능한 경우
- SQS/SNS: AWS 환경이고 운영 부담을 줄이고 싶을 때 (스타트업·소규모 팀에 기본 선택). 단, SQS FIFO는 기본 batching 기준 초당 3,000 messages per API method이고, high throughput FIFO를 켜면 조건부로 더 높일 수 있으므로 “무제한 고처리량 스트림”으로 오해하면 안 된다.
📖 더 보기: Apache Kafka vs RabbitMQ vs AWS SNS/SQS - Ably — 세 브로커의 아키텍처 차이와 시나리오별 선택 기준 (중급) 📖 AWS 공식 수치: Amazon SQS queue types — FIFO 처리량, visibility timeout, deduplication 동작 기준
패턴 2: adjoe 실제 운영 사례 (하루 5억 요청 처리)
독일 애드테크 기업 adjoe는 SNS+SQS로 하루 5억 건 이상의 요청을 처리한다:
규모: 130개 SNS Topic, 300개 이상 SQS Queue가장 바쁜 Queue: 하루 50만 건 메시지 처리선택 이유: AWS 관리형 → 높은 가용성, 실질적으로 무제한 스케일링
핵심 교훈:- 서비스가 130개 이벤트 타입을 발행 → 각 이벤트마다 독립적인 SNS Topic- 소비자(SQS Queue)가 300개 → 같은 이벤트도 팀/목적별로 독립 처리- 직접 연결 대신 브로커를 두어 장애 격리 달성패턴 3: NestJS Scalable Event-Driven Notification System
// 실제 프로덕션에서 AWS SQS + NestJS로 알림 시스템 구축 패턴// 특징: 각 알림 채널(이메일/SMS/푸시)을 독립 Worker로 분리
// 1. SNS에 이벤트 발행 (OrderService)await snsClient.publish({ TopicArn: process.env.ORDER_EVENTS_TOPIC_ARN, Message: JSON.stringify({ orderId, userId, type: "order.created" }), MessageAttributes: { eventType: { DataType: "String", StringValue: "order.created" }, },});
// 2. 각 채널별 SQS Worker (독립 ECS 서비스로 배포)// - email-notification-worker: SES로 이메일 발송// - push-notification-worker: FCM으로 앱 푸시// - slack-notification-worker: Slack 웹훅 전송
// 장점: 이메일 발송이 느려도 푸시/Slack에 영향 없음// 장점: 각 채널 Worker를 부하에 따라 독립 스케일링 가능4. 실무에서 어디에 쓰이나
섹션 제목: “4. 실무에서 어디에 쓰이나”- 서비스 간 통신 (주문 생성 → 결제, 알림, 재고 차감 동시 처리)
- 시스템 간 데이터 동기화
- 감사 로그(Audit Log) 기록
- Nest.js 내부 모듈 간 이벤트 통신
5. 현재 내 업무와 연결점
섹션 제목: “5. 현재 내 업무와 연결점”- 팀 서비스에서
@OnEvent또는 메시지 브로커 사용 여부 파악 - 서비스 간 직접 API 호출이 많다면 EDA로 개선 가능한 부분 식별
- 장애 전파 문제가 있는 구조를 EDA로 개선하는 제안
6. 자주 헷갈리는 개념 비교
섹션 제목: “6. 자주 헷갈리는 개념 비교”| 개념 A | 개념 B | 차이점 |
|---|---|---|
| EDA | MSA | MSA는 서비스 분리 아키텍처, EDA는 서비스 간 통신 방식 (함께 쓰임) |
| Queue | Pub/Sub | Queue는 1:1 작업 처리, Pub/Sub은 1:N 이벤트 브로드캐스트 |
| 동기 API 호출 | 이벤트 발행 | 동기는 응답을 기다리고, 이벤트는 발행 후 즉시 진행 |
| EDA | CQRS | CQRS는 읽기/쓰기 분리 패턴, EDA와 함께 쓰이는 경우 많음 |
| Outbox Pattern | 직접 이벤트 발행 | Outbox는 DB 저장과 이벤트 발행을 원자적으로 묶어 유실 방지 |
| SNS | EventBridge | SNS는 단순 팬아웃, EventBridge는 복잡한 패턴 매칭과 라우팅 지원 |
6.5. 트러블슈팅
섹션 제목: “6.5. 트러블슈팅”🔧 @OnEvent 리스너가 실행되지 않는다
섹션 제목: “🔧 @OnEvent 리스너가 실행되지 않는다”증상: eventEmitter.emit('order.created', payload)를 호출했는데 @OnEvent('order.created') 핸들러가 호출되지 않음
원인: EventEmitterModule.forRoot()를 AppModule의 imports에 등록하지 않았거나, 핸들러가 있는 Service를 providers에 등록하지 않음
해결:
app.module.ts에EventEmitterModule.forRoot()import 여부 확인- 핸들러 Service가 해당 모듈의
providers에 등록되어 있는지 확인 - 이벤트 이름 대소문자/오타 확인 (
order.createdvsorder.Created)
🔧 동일한 이벤트가 중복 처리된다 (SQS)
섹션 제목: “🔧 동일한 이벤트가 중복 처리된다 (SQS)”증상: 주문 생성 이벤트 1건에 결제가 2번 처리됨
원인: SQS는 at-least-once 전달을 보장하므로 네트워크 오류 시 같은 메시지를 2번 전달할 수 있음. Worker가 메시지 처리 후 ack(삭제) 전에 타임아웃이 나면 다시 Queue에 돌아옴
해결:
- 처리 로직을 멱등성(idempotent) 있게 구현 — 동일 orderId로 이미 결제가 됐으면 스킵
- 처리 완료된 이벤트 ID를 DB/Redis에 기록하고 중복 체크
- SQS Visibility Timeout을 처리 시간보다 넉넉하게 설정 (기본 30초)
🔧 이벤트 핸들러 에러가 전체 앱을 죽인다
섹션 제목: “🔧 이벤트 핸들러 에러가 전체 앱을 죽인다”증상: @OnEvent 핸들러에서 예외가 발생했더니 서버 프로세스가 다운됨
원인: Nest.js 내장 EventEmitter는 기본적으로 핸들러 에러를 상위로 전파함. try-catch 없이 예외를 던지면 uncaught exception이 됨
해결:
@OnEvent('order.created')async handleOrderCreated(payload: OrderCreatedEvent) { try { await this.processOrder(payload); } catch (error) { // 에러를 삼키고 로깅 — 앱이 죽지 않도록 this.logger.error(`order.created 처리 실패: ${error.message}`, error.stack); // 필요하면 DLQ(Dead Letter Queue)로 이동하거나 재시도 로직 추가 }}🔧 이벤트 발행은 성공했는데 DB 저장이 실패해서 데이터가 꼬인다
섹션 제목: “🔧 이벤트 발행은 성공했는데 DB 저장이 실패해서 데이터가 꼬인다”증상: SNS 이벤트는 발행됐는데 DB 트랜잭션이 롤백되어 DB에는 데이터가 없음. 구독자 서비스는 이미 처리 완료. 원인: 이벤트 발행과 DB 저장이 별개 트랜잭션으로 분리되어 있어 원자성이 없음 해결:
- Outbox Pattern 도입 — DB 트랜잭션 안에서 이벤트 레코드를 outbox 테이블에 함께 저장
- 별도 프로세스가 outbox 테이블을 읽어서 이벤트 발행 (분리된 신뢰 경계 없음)
- 단기 해결책: 이벤트 발행 전에 DB 저장이 완전히 커밋됐는지 확인 후 발행
🔧 SNS → SQS 팬아웃 시 일부 구독자만 메시지를 받는다
섹션 제목: “🔧 SNS → SQS 팬아웃 시 일부 구독자만 메시지를 받는다”증상: SNS에 이벤트를 발행했는데 3개 SQS 중 1개만 메시지를 받음
원인 1: SQS Queue에 SNS Topic을 구독하는 설정이 빠졌거나, SNS Subscription이 Pending confirmation 상태
원인 2: SQS Queue의 Access Policy에 SNS가 메시지를 보낼 권한이 없음
해결:
- SNS → Topics → 해당 Topic → Subscriptions 탭에서 모든 구독 상태
Confirmed여부 확인 - SQS Queue → Access Policy 확인 — SNS Topic ARN에서
sqs:SendMessage허용 여부 점검:{"Effect": "Allow","Principal": { "Service": "sns.amazonaws.com" },"Action": "sqs:SendMessage","Resource": "arn:aws:sqs:ap-northeast-2:123456:my-queue","Condition": {"ArnEquals": {"aws:SourceArn": "arn:aws:sns:ap-northeast-2:123456:order-events"}}} - 콘솔에서 SNS → Publish message로 테스트 메시지 수동 발행해서 어떤 Queue에 도달하는지 확인
🔧 EventBridge 규칙에 이벤트가 도달했는데 타깃이 실행되지 않는다
섹션 제목: “🔧 EventBridge 규칙에 이벤트가 도달했는데 타깃이 실행되지 않는다”증상: EventBridge 규칙의 Invocations 지표는 증가하는데, SQS/Lambda 타깃에서 처리가 안 됨
원인 1: EventBridge가 타깃(SQS, Lambda 등)을 호출할 권한이 없음 (리소스 기반 정책 누락)
원인 2: 이벤트 패턴(Event Pattern)이 실제 이벤트 구조와 맞지 않아 필터링됨
조용한 실패: 이벤트가 어떤 Rule과도 매칭되지 않으면 타깃 에러가 아니라 No Rules Matched에 가까운 상태가 된다. EventBridge event bus 로그는 기본값이 OFF이고, 활성화해도 best effort라서 CloudWatch 지표와 패턴 테스트를 같이 봐야 한다.
해결:
- EventBridge → Rules → 해당 Rule → Monitoring 탭에서
FailedInvocations지표 확인 - SQS Queue Access Policy에 EventBridge가 메시지를 보낼 수 있는 권한 추가:
{"Effect": "Allow","Principal": { "Service": "events.amazonaws.com" },"Action": "sqs:SendMessage","Resource": "arn:aws:sqs:ap-northeast-2:123456:my-queue"}
- EventBridge 콘솔 → “Test event pattern” 기능으로 실제 이벤트 JSON을 붙여넣어 패턴 일치 여부 검증
- CLI로도 패턴 매칭을 재현:
예상 출력:
Terminal window aws events test-event-pattern \--event-pattern file://event-pattern.json \--event file://sample-event.json{"Result": true}.false면 권한 문제가 아니라 Rule 패턴이 실제 이벤트 구조와 맞지 않는 것이다. - EventBridge 상세 로깅 활성화: Event bus → Logging → CloudWatch Logs 대상으로 설정하고
ERROR또는INFO부터 시작한다.TRACE는Rule Matched,Invocation Attempt Started,No Rules Matched까지 더 많이 남기지만 로그 비용과 민감 정보 노출 가능성을 함께 검토한다.
출처: https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-bus-logs.html, https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-event-logs-execution-steps.html
🔧 Outbox Pattern 구현 후 이벤트가 중복 발행된다
섹션 제목: “🔧 Outbox Pattern 구현 후 이벤트가 중복 발행된다”증상: Outbox 테이블에서 이벤트를 읽어 발행하는 Processor가 동일한 이벤트를 두 번 발행함 원인: Outbox Processor를 여러 인스턴스로 스케일아웃한 경우, 여러 인스턴스가 같은 outbox 레코드를 동시에 읽고 발행할 수 있다. 또는 Processor가 실패 후 재시작하면서 이미 발행했던 레코드를 다시 처리함 해결:
-
SELECT ... FOR UPDATE SKIP LOCKED패턴으로 분산 잠금 구현:-- Outbox Processor에서 처리할 레코드 선점 (다른 인스턴스가 못 가져가게)BEGIN;SELECT id, event_type, payloadFROM outbox_eventsWHERE status = 'PENDING'ORDER BY created_at ASCLIMIT 10FOR UPDATE SKIP LOCKED; -- 다른 트랜잭션이 잠근 행은 건너뜀-- 처리 후 상태 변경UPDATE outbox_events SET status = 'PROCESSED', processed_at = NOW()WHERE id IN (...);COMMIT; -
TypeORM에서 SKIP LOCKED 사용:
const events = await this.dataSource.createQueryBuilder(OutboxEvent, "e").where("e.status = :status", { status: "PENDING" }).orderBy("e.createdAt", "ASC").limit(10).setLock("pessimistic_write_or_fail") // FOR UPDATE SKIP LOCKED.getMany(); -
이벤트 발행 자체의 멱등성도 보장: SNS
MessageDeduplicationId(FIFO Topic) 또는 EventBridge의 이벤트 ID를 Consumer 쪽에서 중복 체크
7. 체크리스트
섹션 제목: “7. 체크리스트”- EDA가 직접 호출 방식과 어떻게 다른지 설명할 수 있다
- Queue(1:1)와 Pub/Sub(1:N)의 차이를 설명할 수 있다
- Nest.js에서
@OnEvent데코레이터의 동작 방식을 설명할 수 있다 - 팀 서비스에서 EDA 패턴이 쓰이는 곳을 찾을 수 있다
- Outbox Pattern이 왜 필요한지 설명할 수 있다
- SNS / SQS / EventBridge 중 어떤 상황에 무엇을 쓸지 설명할 수 있다
8. 추가 학습 키워드
섹션 제목: “8. 추가 학습 키워드”CQRS, Event Sourcing, Saga Pattern, Outbox Pattern, AWS SNS+SQS Fan-out, Kafka Consumer Group, EventBridge
8.5. 추천 리소스
섹션 제목: “8.5. 추천 리소스”- 📖 NestJS 공식 문서: Events —
@OnEvent,EventEmitter2설정 및 사용법 공식 레퍼런스 (입문) - 📖 Event-Driven Architecture with NestJS - DEV Community — 내장 EventEmitter로 EDA 구현하는 실습 예제 (입문)
- 📖 AWS SNS+SQS Fan-out 패턴 구현 가이드 — SNS → 여러 SQS로 팬아웃하는 AWS 실전 패턴 (중급)
- 📖 Outbox & Saga Pattern on AWS EDA — 이벤트 유실 방지(Outbox)와 분산 트랜잭션(Saga)을 AWS 환경에서 구현하는 방법 (중급)
- 📖 SNS vs SQS vs EventBridge 선택 기준 - AWS 공식 문서 — 세 서비스의 차이점과 사용 시나리오별 결정 기준 공식 가이드 (중급)
9. 내가 직접 확인해볼 것
섹션 제목: “9. 내가 직접 확인해볼 것”- 팀 코드에서
@OnEvent, EventEmitter, 메시지 브로커 사용 여부 확인예상 출력: 사용 중이면Terminal window # 팀 프로젝트에서 검색grep -r "@OnEvent\|EventEmitter\|ClientProxy" src/src/orders/order.listener.ts:5:@OnEvent('order.created')형태 - 서비스 간 직접 HTTP 호출이 있는 부분 → EDA로 바꾸면 어떨지 생각
- 팀 코드에서
EventEmitter2또는ClientProxyimport를 검색해서 이벤트 흐름 다이어그램 그려보기 - AWS 콘솔에서 SNS Topics 목록 확인
경로:
SNS → Topics→ 현재 사용 중인 Topic과 각 Topic의 Subscription 수 확인 - EDA 도입 적합성 자가 진단 — 팀 서비스의 특정 흐름에 대해 아래 3가지를 확인:
- “서비스 간 응답 시간 결합이 필요 없는가?” → 주문 생성 후 알림을 굳이 기다릴 필요 없으면 EDA 적합
- “처리가 실패해도 재시도로 복구 가능한가?” → 멱등하게 구현할 수 있으면 EDA 적합
- “하나의 이벤트에 여러 수신자가 관심을 가지는가?” → 결제·알림·재고 모두 주문 이벤트를 원하면 EDA 적합
- 위 3가지 모두 “예”가 아니라면 동기 REST API가 더 단순한 선택일 수 있다
10. 5줄 요약
섹션 제목: “10. 5줄 요약”- EDA는 이벤트 발행/구독으로 서비스 간 느슨한 결합을 만드는 아키텍처다
- Publisher는 누가 구독하는지, Subscriber는 누가 발행했는지 몰라도 된다
- Queue(1:1)는 작업 처리, Pub/Sub(1:N)은 이벤트 브로드캐스트에 사용한다
- Nest.js는 내장 EventEmitter와 외부 메시지 브로커 모듈을 모두 지원한다
- 마이크로서비스에서 장애 전파를 막는 핵심 패턴이다
11. 실전 장애 대응 시나리오 (On-Call Runbook)
섹션 제목: “11. 실전 장애 대응 시나리오 (On-Call Runbook)”EDA 구조에서 자주 발생하는 장애 패턴과 on-call 대응 절차
시나리오 A: “특정 이벤트가 처리되지 않는다”는 제보를 받았을 때
섹션 제목: “시나리오 A: “특정 이벤트가 처리되지 않는다”는 제보를 받았을 때”Step 1. 이벤트가 브로커에 도달했는지 확인 (2분) AWS SNS의 경우: SNS → Topics → 해당 Topic → Monitoring 확인 지표: NumberOfMessagesPublished (발행 수) → 발행 수가 0이면 Publisher 쪽 문제
AWS SQS의 경우: SQS → 해당 Queue → Monitoring 확인 지표: NumberOfMessagesSent, NumberOfMessagesReceived → Sent는 있는데 Received가 0이면 Consumer(Worker)가 폴링을 안 하는 것
Step 2. DLQ 확인 (2분) SQS → DLQ Queue → ApproximateNumberOfMessagesVisible → DLQ에 메시지가 있으면 Consumer가 처리 실패한 것 → DLQ 메시지 샘플 한 건 확인 → Body 내용으로 어떤 이벤트인지 파악
Step 3. Consumer(Worker) 상태 확인 (3분) ECS → Clusters → Worker 서비스 → Tasks → STOPPED 태스크가 있으면 로그 확인 (왜 죽었는지) → CloudWatch Log Insights에서 Worker 로그 에러 검색
Step 4. 임시 복구 Worker 재시작 → DLQ Redrive(원본 큐로 재전송) → 처리 재개 확인시나리오 B: “Poison Pill” 메시지로 Consumer가 무한 실패할 때
섹션 제목: “시나리오 B: “Poison Pill” 메시지로 Consumer가 무한 실패할 때”증상: 특정 메시지 때문에 Consumer가 계속 실패하고, 정상 메시지도 처리 못하게 됨
Poison Pill이란?→ 파싱 불가 데이터, 잘못된 형식, 비즈니스 로직에서 처리 불가한 특수 케이스→ Visibility Timeout 후 계속 재노출 → Consumer가 계속 실패 → DLQ 도달
즉각 조치:1. DLQ에서 해당 메시지 내용 확인 SQS → DLQ → Send and receive messages → Receive message → Body 내용 복사 후 파싱 시도
2. 정상 처리가 불가한 케이스라면: → DLQ에서 해당 메시지 수동 삭제 → 또는 별도 처리 스크립트로 예외 처리 후 삭제
3. 근본 해결: → Consumer 코드에 예외 처리 강화 (파싱 실패 시 로그 남기고 스킵) → Schema 검증 로직 추가 (Consumer 진입 시 메시지 유효성 먼저 체크)
예방 패턴:@SqsMessageHandler('order-queue', false)async handleOrder(message: Message) { try { const payload = JSON.parse(message.Body!); // payload 검증 (Zod, class-validator 등) await this.processOrder(payload); } catch (parseError) { // 파싱/검증 실패는 재시도해도 동일 → DLQ로 즉시 보냄 this.logger.error('파싱 실패 - 재시도 불필요', { body: message.Body }); throw parseError; // throw해서 DLQ로 이동시킴 }}시나리오 C: “SNS → SQS 팬아웃이 일부만 동작할 때”
섹션 제목: “시나리오 C: “SNS → SQS 팬아웃이 일부만 동작할 때””증상: SNS에 이벤트를 발행했는데 3개 SQS 구독 중 1개만 메시지를 받음
확인 순서:1. SNS → Topics → Subscriptions 탭 → 모든 구독의 Status가 "Confirmed"인지 확인 → "PendingConfirmation" 있으면 다시 구독 설정 필요
2. 누락된 SQS Queue의 Access Policy 확인 SQS → 해당 Queue → Access policy → SNS Topic ARN에서 sqs:SendMessage를 허용하는 Policy 있는지 확인 → 없으면 아래 Policy 추가 필요: Principal: sns.amazonaws.com Action: sqs:SendMessage Condition: ArnEquals aws:SourceArn = [SNS Topic ARN]
3. 메시지 필터링 확인 SNS Subscription → Filter policy가 설정된 경우 → 발행 메시지의 MessageAttributes와 Filter policy가 매칭되는지 확인2025년 최신 동향
섹션 제목: “2025년 최신 동향”EventBridge Pipes (2024~2025 주목 기능)
EventBridge Pipes는 이벤트 소스(SQS, DynamoDB Streams 등)와 이벤트 타깃(Lambda, SQS 등)을 코드 없이 직접 연결하는 서비스다. 기존에는 Lambda를 통해 변환해야 했던 것을 Pipe의 필터링/변환 단계로 처리한다.
기존 방식:SQS → Lambda (필터링/변환 코드) → 다른 SQS/SNS
EventBridge Pipes 방식:SQS ──[Pipe]──→ EventBridge Target └─ 필터링: messageType = "order.created"만 └─ 변환: input template으로 필드 재구성 └─ 타깃: 다른 SQS / EventBridge Bus / Lambda
이점: Lambda 없이 이벤트 라우팅 가능 → 인프라 단순화Schema Registry (이벤트 계약 표준화)
대규모 EDA에서 이벤트 스키마 관리가 핵심 과제가 됐다. EventBridge Schema Registry를 사용하면 이벤트 형식을 중앙에서 관리하고, TypeScript 타입을 자동 생성할 수 있다. 팀 간 이벤트 계약(Contract)을 코드로 명시하는 방향이 2025년 업계 표준으로 자리잡는 중이다.
Dead-Letter Queue 14일 보존 원칙
프로덕션에서는 “DLQ Retention은 가능한 최대 14일로 설정”하는 쪽이 안전하다. AWS SQS의 기본 메시지 보존 기간은 4일이고, 설정 가능한 최대값은 14일이다. 원본 큐에서 이미 며칠 동안 재시도한 메시지가 DLQ로 이동하면 조사 시간이 짧아질 수 있으므로, 장애 분석용 DLQ는 원본 큐보다 길게 잡는다.
# DLQ Retention 14일로 설정aws sqs set-queue-attributes \ --queue-url https://sqs.ap-northeast-2.amazonaws.com/123456/email-dlq \ --attributes MessageRetentionPeriod=1209600 # 14일 = 1209600초설정 확인:
aws sqs get-queue-attributes \ --queue-url https://sqs.ap-northeast-2.amazonaws.com/123456/email-dlq \ --attribute-names MessageRetentionPeriod예상 출력: "MessageRetentionPeriod": "1209600". "345600"(4일)이면 운영자가 주말이나 휴일 이후에 DLQ를 열었을 때 메시지가 이미 만료될 수 있다.
📖 더 보기: Event-Driven Architecture: Production Pitfalls & Fixes — EDA를 실제 프로덕션에서 운영할 때 마주치는 함정들과 해결법 (중급)