Notes
K8s 15. Event-Driven Microservices
Event Driven Architecture의 producer, broker, consumer, pub/sub, event streaming, schema versioning, idempotency, eventual consistency, Saga, Outbox, DLQ, Kubernetes 연계 방식을 정리한다.
- Published
- Updated
- Area
- Cloud Infrastructure
- Type
- concept
- Series
- Kubernetes Essentials
- Category
- Notes
Event Driven Architecture, EDA는 시스템 내부에서 발생한 의미 있는 변화인 event를 중심으로 service들을 느슨하게 연결하는 architecture pattern이다. Producer가 event를 발행하면, consumer들은 broker나 event router를 통해 event를 받아 비동기적으로 처리한다.
전통적인 request/response 방식에서는 한 service가 다른 service를 직접 호출하고 응답을 기다린다.
Service A
↓ request
Service B
↓ response
Service A
반면 Event Driven Architecture에서는 어떤 일이 발생하면 event가 발행되고, 그 event에 관심 있는 service들이 각자 반응한다.
Event Producer
↓ publishes event
Event Broker / Event Router
↓
Event Consumer A
Event Consumer B
Event Consumer C
EDA는 cloud native system에서 microservices decoupling, real-time processing, streaming analytics, serverless trigger, 비동기 workflow를 설계할 때 중요한 기반이 된다.
핵심 요약
Event Driven Architecture는 producer가 의미 있는 상태 변화를 event로 발행하고, consumer들이 broker를 통해 이를 비동기적으로 처리함으로써 microservices를 느슨하게 결합하고 real-time 반응성과 확장성을 높이는 cloud native architecture pattern이다.
핵심 구성은 다음이다.
Event Producer
→ event를 생성하고 발행
Event Broker / Router
→ event를 전달, 저장, routing, fan-out
Event Consumer
→ event를 받아 business logic 수행
전체 흐름은 다음과 같다.
Producer
↓ publishes event
Broker / Router
↓
Consumer A
Consumer B
Consumer C
기존 request/response 방식은 단순하고 즉각적인 요청 처리에 적합하다. 반면 EDA는 다음 상황에 강하다.
microservices 간 coupling 감소
real-time processing
streaming analytics
serverless trigger
event replay
비동기 workflow
traffic spike buffering
pluggable microservices
하지만 EDA는 공짜가 아니다. 제대로 운영하려면 다음을 반드시 설계해야 한다.
event schema
event versioning
idempotency
delivery guarantee
ordering
consumer lag
DLQ
retry policy
observability
security
eventual consistency
Saga / Outbox pattern
Event란 무엇인가?
EDA에서 가장 먼저 잡아야 할 개념은 event다.
Event는 시스템 안에서 발생한 의미 있는 사실이다.
사용자가 회원가입을 완료했다.
주문이 생성되었다.
결제가 승인되었다.
상품 재고가 부족해졌다.
파일이 object storage에 업로드되었다.
센서 온도가 임계값을 넘었다.
Pod가 CrashLoopBackOff 상태가 되었다.
새로운 로그인이 감지되었다.
중요한 점은 event는 보통 이미 발생한 사실을 표현한다는 것이다.
좋은 event 이름:
OrderCreated
PaymentApproved
InventoryReserved
UserRegistered
FileUploaded
ShipmentDispatched
애매하거나 좋지 않은 event 이름:
CreateOrder
ApprovePayment
ReserveInventory
SendEmail
차이는 다음이다.
OrderCreated
→ 주문이 생성되었다는 사실
CreateOrder
→ 주문을 생성하라는 지시
EDA에서 event는 command와 구분해야 한다.
| 구분 | 의미 | 예시 |
|---|---|---|
| Command | 어떤 일을 하라고 지시 | CreateOrder, CancelPayment |
| Event | 어떤 일이 이미 발생했다는 사실 | OrderCreated, PaymentCancelled |
이 구분이 흐려지면 event-driven system이 아니라 command bus처럼 변질될 수 있다.
Request/Response 방식의 한계
전통적인 application은 request/response 구조로 많이 설계된다.
예를 들어 주문 API가 있다고 하자.
Client
↓ POST /orders
order-service
↓ call payment-service
payment-service
↓ response
order-service
↓ call inventory-service
inventory-service
↓ response
order-service
↓ call notification-service
notification-service
↓ response
Client
이 구조는 단순하고 이해하기 쉽다. 하지만 서비스가 늘어나면 문제가 생긴다.
order-service가 payment-service를 알아야 한다.
order-service가 inventory-service를 알아야 한다.
order-service가 notification-service를 알아야 한다.
하나의 downstream service가 느리면 전체 API latency가 증가한다.
어느 하나가 장애나면 주문 생성 전체가 실패할 수 있다.
새로운 후속 처리를 추가할 때 order-service 코드를 수정해야 한다.
즉 service 간 coupling이 커진다.
order-service
├─ payment-service
├─ inventory-service
├─ notification-service
├─ analytics-service
└─ recommendation-service
처음에는 괜찮아 보이지만, 시간이 지나면 central orchestration service가 너무 많은 책임을 갖게 된다.
Event-driven 방식으로 바꾸면 어떻게 되는가?
같은 주문 생성 흐름을 EDA로 바꾸면 다음처럼 된다.
Client
↓ POST /orders
order-service
↓ publishes OrderCreated
event broker
├─ payment-service consumes OrderCreated
├─ inventory-service consumes OrderCreated
├─ notification-service consumes OrderCreated
└─ analytics-service consumes OrderCreated
order-service는 더 이상 모든 후속 service를 직접 호출하지 않는다.
order-service는 “주문이 생성되었다”는 event를 발행한다.
{
"eventType": "OrderCreated",
"eventId": "evt-001",
"occurredAt": "2026-06-02T10:00:00Z",
"data": {
"orderId": "ord-123",
"userId": "user-456",
"amount": 50000
}
}
그 event에 관심 있는 service들이 각자 처리한다.
payment-service
→ 결제 프로세스 시작
inventory-service
→ 재고 예약
notification-service
→ 주문 확인 알림 발송
analytics-service
→ 실시간 매출 지표 업데이트
이 구조의 핵심은 producer와 consumer가 직접 연결되지 않는다는 점이다.
producer
→ event를 발행한다.
consumer
→ 관심 있는 event를 구독한다.
producer는 consumer 목록을 몰라도 된다.
consumer는 producer를 직접 호출하지 않아도 된다.
EDA의 핵심 구성요소
EDA는 보통 세 가지 핵심 요소로 설명할 수 있다.
Event Producer
Event Broker / Event Router
Event Consumer
| 구성요소 | 역할 | 예시 |
|---|---|---|
| Event Producer | event를 생성하고 발행 | 주문 서비스, IoT 센서, UI, API, batch job |
| Event Broker / Router | event를 받아 전달, 저장, fan-out | Kafka, RabbitMQ, NATS, EventBridge, MQ |
| Event Consumer | event를 구독하고 처리 | 결제 서비스, 알림 서비스, 분석 서비스 |
흐름은 다음이다.
Producer
↓
Broker / Router
↓
Consumer
조금 더 구체적으로는 다음과 같다.
order-service
↓ publishes OrderCreated
orders topic / event channel
↓
payment-service
inventory-service
notification-service
analytics-service
Event Broker는 왜 필요한가?
Producer가 consumer를 직접 호출하지 않고 broker를 거치는 이유는 decoupling 때문이다.
직접 호출 구조:
order-service
├─ payment-service
├─ inventory-service
├─ notification-service
└─ analytics-service
Broker 기반 구조:
order-service
↓
event broker
├─ payment-service
├─ inventory-service
├─ notification-service
└─ analytics-service
broker가 있으면 producer는 consumer 목록을 몰라도 된다.
producer가 알아야 하는 것:
어떤 event를 발행할지
어느 topic/channel에 발행할지
producer가 몰라도 되는 것:
누가 이 event를 소비하는지
consumer가 몇 개인지
consumer가 어떤 언어로 구현되었는지
consumer가 지금 배포 중인지
이 구조 덕분에 새로운 consumer를 추가할 때 producer를 수정하지 않아도 된다.
예를 들어 나중에 fraud-detection-service를 추가하고 싶다면 다음처럼 하면 된다.
OrderCreated event를 구독하는 fraud-detection-service 추가
order-service 코드는 그대로 둘 수 있다.
Pub/Sub 모델
EDA에서 가장 대표적인 패턴은 publish/subscribe, 즉 pub/sub이다.
Publisher
↓ publishes event
Topic / Channel
↓
Subscriber A
Subscriber B
Subscriber C
예를 들어 OrderCreated topic이 있다고 하자.
Topic: orders.created
├─ payment-service
├─ inventory-service
└─ notification-service
각 subscriber는 같은 event를 받아 자신만의 처리를 한다.
payment-service
→ 결제 처리
inventory-service
→ 재고 예약
notification-service
→ 알림 발송
Pub/Sub 모델의 핵심은 publisher가 subscriber를 몰라도 된다는 점이다.
Publisher:
orders.created topic에 event 발행
Subscriber:
orders.created topic을 구독하고 각자 처리
Queue 모델과 Pub/Sub 모델의 차이
EDA를 이해할 때 queue와 pub/sub를 구분하는 것도 중요하다.
Queue
Queue는 보통 여러 worker 중 하나가 message를 가져가 처리한다.
Queue
├─ worker-1
├─ worker-2
└─ worker-3
하나의 message는 보통 하나의 worker에게 처리된다.
message A → worker-1
message B → worker-2
message C → worker-3
이 모델은 작업 분산에 적합하다.
image resize job
email sending job
background task
batch processing
Pub/Sub
Pub/Sub에서는 하나의 event를 여러 subscriber가 각각 받을 수 있다.
OrderCreated event
├─ payment-service receives
├─ inventory-service receives
└─ notification-service receives
즉 목적이 다르다.
| 모델 | 핵심 목적 | 소비 방식 |
|---|---|---|
| Queue | 작업을 여러 worker에게 분산 | message 하나를 consumer group 내 하나가 처리 |
| Pub/Sub | event를 여러 관심 서비스에 fan-out | 여러 subscriber가 같은 event를 각각 처리 |
| Event Stream | event log를 저장하고 consumer가 offset 기준으로 읽음 | consumer가 원하는 시점부터 재처리 가능 |
실제 시스템에서는 이 모델들이 섞인다. Kafka에서는 topic과 consumer group을 어떻게 구성하느냐에 따라 pub/sub처럼도, queue처럼도 사용할 수 있다.
Event Streaming
Event streaming은 단순 message 전달을 넘어 event를 시간 순서의 log로 저장한다.
event stream / log
offset 0: OrderCreated
offset 1: PaymentApproved
offset 2: InventoryReserved
offset 3: OrderShipped
consumer는 이 stream을 읽는다.
consumer A reads from offset 0
consumer B reads from offset 2
consumer C replays from beginning
Event streaming의 장점은 event를 replay할 수 있다는 점이다.
새로운 analytics-service를 추가한다.
과거 OrderCreated event부터 다시 읽는다.
전체 주문 통계를 재계산한다.
Event streaming은 다음 상황에 유용하다.
real-time analytics
audit log
event replay
consumer 재처리
stream processing
state reconstruction
Event Notification vs Event-Carried State Transfer
EDA에서 event payload를 어떻게 설계할지도 중요하다.
Event Notification
event가 최소 정보만 담는 방식이다.
{
"eventType": "OrderCreated",
"orderId": "ord-123"
}
consumer는 자세한 정보를 얻기 위해 다시 API를 호출한다.
consumer receives OrderCreated
↓
GET /orders/ord-123
장점:
event payload가 작다.
source of truth를 producer service에 둘 수 있다.
schema 변화 부담이 적을 수 있다.
단점:
consumer가 producer API에 다시 의존한다.
event 폭주 시 API 호출도 폭주할 수 있다.
producer 장애 시 consumer가 상세 데이터를 못 가져올 수 있다.
Event-Carried State Transfer
event가 처리에 필요한 상태를 함께 담는다.
{
"eventType": "OrderCreated",
"eventId": "evt-001",
"data": {
"orderId": "ord-123",
"userId": "user-456",
"items": [
{
"productId": "p-1",
"quantity": 2,
"price": 10000
}
],
"totalAmount": 20000
}
}
장점:
consumer가 producer API를 다시 호출하지 않아도 된다.
consumer가 독립적으로 처리할 수 있다.
event replay와 analytics에 유리하다.
단점:
event payload가 커진다.
schema versioning이 중요해진다.
개인정보나 민감정보 포함 위험이 커진다.
실무에서는 event 성격에 따라 둘을 적절히 선택한다.
EDA가 microservices에 주는 장점
Microservices architecture에서 가장 어려운 문제는 service 간 coupling이다.
동기 호출이 많아지면 다음 문제가 생긴다.
service A가 service B를 기다림
service B가 service C를 기다림
service C가 느려지면 전체가 느려짐
한 service 장애가 chain 전체로 전파됨
EDA는 service 간 결합을 줄인다.
producer는 event만 발행한다.
consumer는 관심 있는 event만 구독한다.
서로의 내부 구현을 몰라도 된다.
장점:
독립 배포 가능
consumer 추가가 쉬움
실시간 반응 가능
비동기 처리 가능
traffic spike 흡수 가능
analytics와 streaming 확장 용이
Real-time user interaction
EDA는 real-time 또는 near-real-time interaction을 구성하는 데 유리하다.
예를 들어 e-commerce에서 사용자가 상품을 클릭했다고 하자.
UserClickedProduct
이 event를 여러 service가 소비할 수 있다.
recommendation-service
→ 추천 상품 갱신
analytics-service
→ 실시간 클릭stream 분석
personalization-service
→ 사용자 세션 개인화
marketing-service
→ campaign 반응 측정
request/response 방식에서는 각 service가 필요할 때 데이터를 조회한다.
analytics-service가 주기적으로 DB polling
recommendation-service가 batch로 클릭 로그 처리
EDA에서는 event 발생 즉시 downstream이 반응할 수 있다.
click event 발생
↓
event stream
↓
near-real-time processing
↓
추천/분석/개인화 반영
이것이 real-time 또는 near-real-time interaction의 핵심이다.
Pluggable microservices
EDA의 큰 장점 중 하나는 microservice를 “끼워 넣기” 쉽다는 점이다.
예를 들어 기존에는 주문 생성 후 결제와 재고 처리만 있었다.
OrderCreated
├─ payment-service
└─ inventory-service
나중에 다음 기능을 추가하고 싶다.
fraud detection
recommendation update
coupon analytics
customer segmentation
Request/response 방식에서는 order-service가 이 service들을 추가 호출하도록 수정해야 할 수 있다.
EDA에서는 새 consumer를 추가한다.
OrderCreated
├─ payment-service
├─ inventory-service
├─ fraud-detection-service
├─ analytics-service
└─ segmentation-service
producer는 그대로다.
order-service code change 없음
이것이 pluggable microservices의 핵심이다.
Streaming과 analytics 확장
EDA는 analytics와도 잘 맞는다.
Event stream은 “시스템에서 실제로 일어난 일들의 시간 순서 기록”이다.
UserRegistered
UserLoggedIn
ProductViewed
CartCreated
OrderCreated
PaymentApproved
OrderCancelled
이 event stream을 분석하면 real-time dashboard를 만들 수 있다.
실시간 주문 수
결제 실패율
장바구니 이탈률
상품 조회 수
지역별 traffic
fraud signal
Batch analytics에서는 데이터를 나중에 모아 분석한다.
DB dump
↓
data lake
↓
batch job
↓
report
EDA와 streaming analytics에서는 event가 발생할 때마다 처리할 수 있다.
event 발생
↓
stream processor
↓
dashboard / alert / model update
이 구조는 cloud native 환경에서 실시간 운영 지표, recommendation, fraud detection, IoT monitoring 등에 유용하다.
EDA와 Serverless / FaaS
EDA는 serverless와 매우 잘 맞는다.
Serverless function은 event를 trigger로 실행될 수 있다.
ObjectCreated event
↓
image-resize function 실행
OrderCreated event
↓
notification function 실행
Timer event
↓
batch cleanup function 실행
구조는 다음과 같다.
Event Source
↓
Event Broker / Event Router
↓
Function
장점:
event가 없으면 실행하지 않음
event가 발생하면 자동 실행
작은 단위의 processing logic 구성 가능
scale-to-zero와 잘 맞음
하지만 주의할 점도 있다.
cold start
timeout 제한
state 관리 어려움
event 중복 처리 가능성
관측성 복잡도
function sprawl
즉 EDA와 serverless는 잘 맞지만, 모든 event processing을 무조건 function으로 만들면 운영 복잡도가 커질 수 있다.
EDA와 Kubernetes
Kubernetes 자체도 event와 control loop 중심의 시스템이다.
Kubernetes 내부에서는 다음과 같은 event/state 변화가 발생한다.
Pod created
Pod scheduled
Pod failed
Deployment updated
Node NotReady
PVC bound
Controller들은 API Server를 watch하면서 변화에 반응한다.
Deployment Controller
→ Deployment 변경 감지
→ ReplicaSet 생성/수정
ReplicaSet Controller
→ Pod 수 부족 감지
→ Pod 생성
kube-scheduler
→ nodeName 없는 Pod 감지
→ Node binding
kubelet
→ 자기 Node에 할당된 Pod 감지
→ container 실행
즉 Kubernetes는 desired state reconciliation system이고, 내부적으로 event-driven/control-loop적 사고가 강하다.
Application level EDA를 Kubernetes에서 구현할 때는 다음 도구들이 자주 등장한다.
Kafka
RabbitMQ
NATS
Redis Streams
Knative Eventing
KEDA
Strimzi
Argo Events
예를 들어 KEDA는 queue length, Kafka lag 같은 event source metric을 기반으로 Kubernetes workload를 scale할 수 있다.
Kafka topic lag 증가
↓
KEDA ScaledObject
↓
Deployment replicas 증가
Knative Eventing은 event source, broker, trigger, sink 구조로 event-driven application을 Kubernetes-native하게 구성할 수 있다.
EDA의 설계 핵심: loose coupling
EDA의 핵심 가치는 loose coupling이다.
하지만 coupling이 완전히 사라지는 것은 아니다. 형태가 바뀐다.
동기 호출의 coupling:
service endpoint coupling
availability coupling
latency coupling
API contract coupling
EDA의 coupling:
event schema coupling
topic/channel coupling
event semantics coupling
ordering/consistency coupling
즉 EDA는 coupling을 줄이지만, event contract 관리는 더 중요해진다.
이 event는 무엇을 의미하는가?
언제 발행되는가?
한 번만 발행되는가?
중복 발행될 수 있는가?
payload schema는 무엇인가?
consumer는 어떤 순서 보장을 기대할 수 있는가?
event version은 어떻게 관리하는가?
이 질문에 답하지 않으면 EDA는 금방 혼란스러워진다.
Event schema와 versioning
Event는 contract다. 따라서 schema가 필요하다.
예시:
{
"eventType": "PaymentApproved",
"eventVersion": "1.0",
"eventId": "evt-123",
"occurredAt": "2026-06-02T10:00:00Z",
"data": {
"paymentId": "pay-123",
"orderId": "ord-123",
"amount": 50000,
"currency": "KRW"
}
}
Event schema에서 중요한 field:
eventId
eventType
eventVersion
occurredAt
producer
correlationId
causationId
data
Versioning 원칙:
optional field 추가는 대체로 안전
required field 추가는 위험
field 삭제는 breaking change
field 의미 변경은 매우 위험
eventType 재사용 금지
좋은 전략:
schema registry 사용
consumer-driven contract test
event version 명시
backward compatibility 검증
deprecated field 운영 기간 부여
Delivery guarantee
EDA에서 반드시 이해해야 할 것이 delivery guarantee다.
대표적인 전달 보장은 다음과 같다.
| 보장 방식 | 의미 | 주의점 |
|---|---|---|
| At-most-once | 최대 한 번 전달 | 유실 가능 |
| At-least-once | 최소 한 번 전달 | 중복 가능 |
| Exactly-once | 정확히 한 번처럼 처리 | 구현과 조건이 복잡 |
많은 시스템에서는 at-least-once가 현실적인 기본값이다.
event는 최소 한 번 전달된다.
따라서 consumer는 같은 event를 여러 번 받을 수 있다.
그래서 consumer는 idempotent해야 한다.
같은 event를 두 번 처리해도 결과가 깨지지 않아야 한다.
예를 들어 결제 승인 event를 두 번 받았을 때 포인트를 두 번 지급하면 안 된다.
PaymentApproved evt-123
↓
point-service
↓
eventId evt-123 처리 여부 확인
↓
이미 처리했으면 skip
Idempotency
EDA에서 idempotency는 필수에 가깝다.
나쁜 처리:
OrderCreated event 수신
↓
무조건 배송 요청 생성
같은 event가 두 번 오면 배송 요청이 두 개 생길 수 있다.
좋은 처리:
OrderCreated event 수신
↓
eventId 또는 orderId 기준으로 이미 처리했는지 확인
↓
처리되지 않았으면 배송 요청 생성
↓
처리 기록 저장
idempotency key 예시:
eventId
orderId + consumerName
business transaction ID
Consumer table 예시:
processed_events
- event_id
- consumer_name
- processed_at
EDA를 운영할 때 “event는 중복될 수 있다”는 가정으로 consumer를 설계하는 것이 안전하다.
Ordering
Event ordering도 중요한 문제다.
예를 들어 다음 event가 있다.
OrderCreated
PaymentApproved
OrderCancelled
OrderShipped
consumer가 순서를 잘못 받으면 문제가 생긴다.
OrderShipped를 먼저 처리
OrderCreated를 나중에 처리
Ordering은 보통 partition key에 따라 보장된다.
예를 들어 Kafka에서는 같은 key를 가진 event를 같은 partition에 넣으면 해당 partition 안에서 순서를 유지할 수 있다.
key = orderId
그러면 같은 주문에 대한 event는 같은 partition에 들어갈 수 있다.
orderId=ord-123
→ OrderCreated
→ PaymentApproved
→ OrderShipped
하지만 전체 system의 global ordering은 어렵고 비용이 크다.
실무에서는 다음처럼 생각하는 것이 좋다.
전체 event의 완전한 순서 보장
→ 거의 불필요하거나 비싸다.
business entity 단위의 순서 보장
→ orderId, userId, accountId 기준으로 설계한다.
Consistency와 eventual consistency
EDA는 비동기 구조이므로 eventual consistency가 자주 등장한다.
예를 들어 주문 생성 직후 inventory-service가 아직 재고 예약을 끝내지 않았을 수 있다.
OrderCreated 발행
↓
client는 주문 생성 응답 받음
↓
inventory-service가 event 처리 중
↓
잠시 후 InventoryReserved 발행
이때 system은 일시적으로 불일치 상태일 수 있다.
order-service:
order status = CREATED
inventory-service:
reservation not yet completed
잠시 후 consistency가 맞춰진다.
order status = CONFIRMED
inventory reservation = RESERVED
이런 eventual consistency를 사용자 경험과 business rule에 맞게 설계해야 한다.
주문 생성 직후 “주문 접수됨” 표시
결제/재고 완료 후 “주문 확정됨” 표시
실패 시 “주문 실패/취소됨” 표시
즉 EDA에서는 상태 전이와 보상 처리를 명확히 해야 한다.
Choreography vs Orchestration
EDA에서는 workflow를 구성하는 방식으로 choreography와 orchestration이 있다.
Choreography
각 service가 event에 반응해 다음 event를 발행한다.
OrderCreated
↓
payment-service processes
↓
PaymentApproved
↓
inventory-service processes
↓
InventoryReserved
↓
shipping-service processes
↓
ShipmentCreated
장점:
중앙 coordinator가 없음
service autonomy 높음
loose coupling 강함
단점:
전체 workflow를 파악하기 어려움
event chain이 복잡해짐
장애 처리 흐름이 흩어짐
Orchestration
중앙 orchestrator가 workflow를 제어한다.
order-orchestrator
↓ command
payment-service
↓ event
order-orchestrator
↓ command
inventory-service
장점:
전체 workflow 가시성 높음
복잡한 business process 제어에 유리
보상 transaction 설계가 명확함
단점:
orchestrator에 책임 집중
central coupling 증가 가능
둘 중 하나만 정답은 아니다.
단순 event reaction
→ choreography 적합
복잡한 multi-step business transaction
→ orchestration 적합
Saga pattern
분산 transaction이 필요한 경우 EDA에서는 Saga pattern을 자주 사용한다.
예를 들어 주문 처리에는 여러 단계가 있다.
1. 주문 생성
2. 결제 승인
3. 재고 예약
4. 배송 요청
중간에 실패하면 보상 작업이 필요하다.
재고 예약 실패
↓
결제 취소
↓
주문 취소
Saga는 각 단계를 local transaction으로 처리하고, 실패 시 compensating action을 수행한다.
OrderCreated
↓
PaymentApproved
↓
InventoryReservationFailed
↓
PaymentRefundRequested
↓
OrderCancelled
EDA에서는 Saga를 choreography로 구현할 수도 있고 orchestration으로 구현할 수도 있다.
Transactional Outbox
EDA에서 매우 중요한 실무 패턴이 Transactional Outbox다.
문제는 다음이다.
DB에 주문 저장 성공
event 발행 실패
또는 반대도 가능하다.
event 발행 성공
DB 저장 실패
이렇게 되면 system state와 event stream이 불일치한다.
Transactional Outbox는 DB transaction 안에서 business data와 outbox event를 함께 저장한다.
BEGIN TRANSACTION
INSERT INTO orders ...
INSERT INTO outbox_events ...
COMMIT
그 후 별도 publisher가 outbox table을 읽어 broker에 event를 발행한다.
outbox_events
↓
outbox publisher
↓
event broker
이 패턴은 “상태 변경과 event 발행의 원자성” 문제를 완화한다.
Dead Letter Queue, DLQ
Consumer가 event 처리를 계속 실패할 수 있다.
예:
잘못된 schema
필수 field 누락
외부 API 계속 실패
consumer bug
무한 재시도하면 system에 부담을 준다.
event 처리 실패
↓
retry
↓
retry
↓
retry
↓
consumer lag 증가
DLQ는 일정 횟수 이상 실패한 event를 별도 queue/topic으로 보내는 구조다.
main topic
↓
consumer 처리 실패 N회
↓
dead-letter topic
운영자는 DLQ를 모니터링해야 한다.
DLQ event 수 증가
→ schema 문제
→ 특정 consumer bug
→ poison message
→ downstream 장애
DLQ는 실패를 숨기는 곳이 아니라, 실패를 격리하고 분석하기 위한 곳이다.
Backpressure와 consumer lag
EDA에서는 producer가 빠르게 event를 발행하고 consumer가 느리게 처리할 수 있다.
producer throughput: 10,000 events/sec
consumer throughput: 2,000 events/sec
그러면 lag가 쌓인다.
event backlog 증가
consumer lag 증가
처리 지연 증가
이때 필요한 전략:
consumer scale-out
partition 수 조정
batch processing
backpressure
rate limit
priority queue
load shedding
Kubernetes에서는 consumer를 horizontal scaling할 수 있다.
kubectl scale deployment order-consumer --replicas=10
또는 KEDA를 사용해 queue length나 Kafka lag 기반으로 자동 확장할 수 있다.
consumer lag 증가
↓
KEDA
↓
consumer Deployment scale out
EDA의 observability
EDA는 비동기 구조라 observability가 매우 중요하다.
Request/response에서는 call stack이 비교적 명확하다.
Client → API → DB
EDA에서는 흐름이 흩어진다.
OrderCreated
├─ payment-service
├─ inventory-service
├─ notification-service
└─ analytics-service
필요한 observability:
eventId
correlationId
causationId
producer
consumer
topic
partition
offset
processing latency
consumer lag
retry count
DLQ count
Log에는 correlation ID가 있어야 한다.
{
"level": "INFO",
"message": "Processed OrderCreated",
"eventId": "evt-123",
"correlationId": "corr-789",
"consumer": "payment-service"
}
Trace도 event boundary를 따라 연결되어야 한다.
POST /orders
↓ correlationId=corr-789
OrderCreated
↓
payment-service
↓
PaymentApproved
EDA에서 observability가 없으면 장애 발생 시 “어느 consumer가 어느 event를 어디까지 처리했는지” 알기 어렵다.
EDA의 보안
EDA에서는 API 보안뿐 아니라 event pipeline 보안도 필요하다.
확인할 것:
broker authentication
producer authorization
consumer authorization
topic-level ACL
event payload encryption
PII masking
schema validation
audit log
secret management
network isolation
mTLS
예를 들어 모든 service가 모든 topic을 읽을 수 있으면 위험하다.
notification-service가 payment event의 민감정보까지 읽을 수 있음
analytics-service가 개인정보를 과도하게 수집
좋은 설계:
producer는 필요한 topic에만 publish
consumer는 필요한 topic만 subscribe
민감정보는 event payload에 최소화
PII는 masking/tokenization
topic별 ACL 적용
EDA에서도 least privilege 원칙이 필요하다.
EDA의 장점
EDA의 장점은 다음과 같다.
| 장점 | 설명 |
|---|---|
| Loose coupling | producer와 consumer가 직접 연결되지 않음 |
| 비동기 처리 | producer가 consumer 처리를 기다리지 않아도 됨 |
| 확장성 | consumer를 독립적으로 scale 가능 |
| 실시간 반응 | event 발생 즉시 downstream 처리 가능 |
| 플러그인식 확장 | 새로운 consumer 추가가 쉬움 |
| 장애 격리 | 일부 consumer 장애가 producer를 직접 막지 않음 |
| streaming analytics | event stream을 실시간 분석 가능 |
| cloud native와 적합 | microservices, serverless, Kubernetes와 잘 맞음 |
EDA의 단점과 주의할 점
EDA는 강력하지만 복잡하다.
| 주의점 | 설명 |
|---|---|
| Eventual consistency | 즉시 일관성이 깨질 수 있음 |
| Debugging 어려움 | 호출 흐름이 분산됨 |
| Schema 관리 필요 | event contract가 깨지면 consumer 장애 |
| 중복 처리 | at-least-once delivery에서는 idempotency 필요 |
| 순서 보장 | ordering 요구사항을 명확히 해야 함 |
| 운영 복잡도 | broker, topic, consumer lag, DLQ 관리 필요 |
| transaction 어려움 | distributed transaction 대신 Saga/Outbox 필요 |
| event storm | 너무 많은 event가 무분별하게 발행될 수 있음 |
따라서 EDA는 “무조건 더 좋은 architecture”가 아니다.
비동기 event가 필요한 곳에 사용해야 한다.
단순 request/response로 충분한 곳에는 억지로 적용하지 않는 것이 좋다.
EDA가 적합한 경우
EDA가 잘 맞는 경우:
여러 service가 같은 business event에 반응해야 한다.
실시간 알림이나 analytics가 필요하다.
microservices 간 coupling을 줄이고 싶다.
비동기 처리가 자연스러운 workflow다.
traffic spike를 buffer로 흡수해야 한다.
event replay가 필요하다.
IoT, clickstream, log stream처럼 event volume이 크다.
serverless function trigger가 필요하다.
예시:
e-commerce 주문 처리
결제 상태 변경
재고 변화
배송 상태 업데이트
IoT sensor stream
fraud detection
real-time recommendation
audit logging
DevOps alerting
file upload processing
EDA가 적합하지 않을 수 있는 경우
EDA가 오히려 과한 경우:
단순 CRUD API
즉시 응답과 강한 일관성이 중요한 transaction
service 수가 적고 직접 호출이 더 단순한 경우
팀이 broker 운영 경험이 없는 경우
event schema/versioning 체계가 없는 경우
observability가 준비되지 않은 경우
예를 들어 단순 관리자 페이지에서 설정 하나를 저장하는 API라면, event-driven 구조보다 request/response + DB transaction이 더 단순하고 안전할 수 있다.
Cloud Native API Solution과 EDA의 관계
Cloud Native API Solution은 API Gateway, microservices, Kubernetes/OpenShift runtime, CI/CD, IaC, observability를 중심으로 한다.
EDA는 여기에 비동기 확장축을 추가한다.
Synchronous API
→ client request에 즉시 응답
→ command/query 처리
Event-driven flow
→ state change를 event로 발행
→ downstream service들이 비동기 처리
두 방식은 경쟁 관계가 아니다.
API
→ 외부 요청을 받는 entrypoint
Event
→ 내부 상태 변화와 side effect를 전파하는 mechanism
예를 들어 다음 구조가 가능하다.
POST /orders
↓
order-service creates order
↓
OrderCreated event published
↓
payment, inventory, notification, analytics process asynchronously
이 구조가 cloud native API + EDA의 전형적인 결합이다.
전체 mental model
EDA를 이해하는 흐름은 다음과 같다.
1. Event는 시스템에서 발생한 의미 있는 상태 변화나 사실이다.
2. Request/response 구조에서는 service가 다른 service를 직접 호출하고 응답을 기다린다.
3. Event Driven Architecture에서는 producer가 event를 발행하고,
consumer들이 broker를 통해 event를 받아 비동기적으로 처리한다.
4. Producer는 consumer를 몰라도 되고,
consumer는 producer를 직접 호출하지 않아도 된다.
5. 이 구조는 microservices를 느슨하게 결합하고,
새로운 consumer를 쉽게 추가할 수 있게 한다.
6. EDA는 real-time interaction, streaming analytics,
serverless trigger, pluggable microservices에 유리하다.
7. 하지만 EDA는 event schema, delivery guarantee, ordering,
idempotency, eventual consistency, observability를 반드시 설계해야 한다.
8. Kubernetes 환경에서는 Kafka, RabbitMQ, NATS, Knative Eventing,
KEDA, Argo Events 같은 도구와 결합해 EDA를 구현할 수 있다.
9. EDA는 request/response를 대체하는 것이 아니라,
cloud native system에서 비동기 반응과 상태 변화 전파를 담당하는 architecture pattern이다.
요약
Event Driven Architecture는 service들이 직접 서로를 호출하는 대신, 시스템에서 발생한 event를 broker나 event router를 통해 발행하고 소비하는 architecture다.
핵심 구성은 다음이다.
Event Producer
→ event를 생성하고 발행
Event Broker / Router
→ event를 전달, 저장, routing, fan-out
Event Consumer
→ event를 받아 business logic 수행
EDA는 microservices decoupling, real-time processing, streaming analytics, serverless trigger, event replay, 비동기 workflow에 강하다. 하지만 event schema, idempotency, ordering, delivery guarantee, eventual consistency, Saga, Outbox, DLQ, observability, security를 함께 설계하지 않으면 운영 복잡도가 빠르게 커진다.
한 문장으로 정리하면 다음과 같다.
Event Driven Architecture는 producer가 의미 있는 상태 변화를 event로 발행하고, consumer들이 broker를 통해 이를 비동기적으로 처리함으로써 microservices를 느슨하게 결합하고 real-time 반응성과 확장성을 높이는 cloud native architecture pattern이다.