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와 외부 메시지 브로커 연동 모듈이 있고, 실무에서 서비스 간 통신 구조를 설계할 때 반드시 마주치는 개념이다.
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가 복구될 때까지 이벤트를 보관한다(내구성). 이 두 가지 속성이 EDA가 MSA 환경에서 표준 패턴이 된 근본 이유다.
2025년 EventBridge 신기능 — 더욱 강력해진 지능형 라우팅
2025년 Amazon EventBridge는 향상된 로깅 기능을 추가했다. 이제 이벤트 전체 라이프사이클(성공/실패/재시도 지표 포함)을 상세히 추적할 수 있다. 또한 Cross-Account 직접 전달이 가능해져, 다른 AWS 계정의 SQS Queue로 직접 이벤트를 전송할 수 있다 (기존에는 중간 인프라가 필요했음).
기존 방식 (크로스 계정):계정 A EventBridge → 계정 A Lambda → 계정 B SQS
2025년 방식 (직접 전달):계정 A EventBridge → 계정 B SQS (직접!)→ 멀티 계정 환경(개발/스테이징/프로덕션 계정 분리)에서 아키텍처가 크게 단순해짐📖 더 보기: 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)은 L8 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)
실무에서 “어떤 브로커를 쓸지”는 자주 나오는 결정이다. 2025년 현재 AWS는 세 가지 옵션을 제공하며, 계층적으로 조합해 쓰는 것이 표준 패턴이다.
| 서비스 | 전달 방식 | 주요 사용 사례 | 선택 기준 |
|---|---|---|---|
| SQS | Pull(폴링) | 비동기 작업 큐, Worker 패턴 | 순서/속도 조절이 필요한 1:1 처리 |
| SNS | Push(즉시) | 실시간 팬아웃, 모바일 푸시, 여러 서비스 동시 알림 | 즉시 1:N 브로드캐스트 |
| EventBridge | Push(라우팅) | 복잡한 이벤트 라우팅, AWS 서비스 간 연동, 스케줄러 | 패턴 매칭, 크로스 계정, 서드파티 연동 |
권장 조합 아키텍처:EventBridge (지능형 라우터, 패턴 매칭) ↓ SNS Topic (팬아웃, 실시간 전달) ┌──┴──┬──────────────┐ ↓ ↓ ↓[SQS] [SQS] [Lambda](버퍼링, 재처리) (즉시 처리)패턴 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 코드를 변경하지 않아도 된다.
패턴 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)이 실제 이벤트 구조와 맞지 않아 필터링됨 해결:
- 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을 붙여넣어 패턴 일치 여부 검증
- 2025년 추가된 EventBridge 상세 로깅 활성화: Rule → “Logging” → CloudWatch Logs 대상으로 설정하면 이벤트 처리 성공/실패 상세 내역 확인 가능
🔧 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 수 확인
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일 보존 원칙
2025년 프로덕션 베스트 프랙티스로 “DLQ Retention은 항상 최대 14일로 설정”이 권장된다. 원본 큐와 같은 보존 기간으로 설정하면 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초📖 더 보기: Event-Driven Architecture: Production Pitfalls & Fixes — EDA를 실제 프로덕션에서 운영할 때 마주치는 함정들과 해결법 (중급)