Back to Notes

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 ArchitectureEDAMicroservicesPub/SubEvent StreamingKafkaRabbitMQKnative EventingKEDASagaOutboxDLQ

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 Producerevent를 생성하고 발행주문 서비스, IoT 센서, UI, API, batch job
Event Broker / Routerevent를 받아 전달, 저장, fan-outKafka, RabbitMQ, NATS, EventBridge, MQ
Event Consumerevent를 구독하고 처리결제 서비스, 알림 서비스, 분석 서비스

흐름은 다음이다.

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/Subevent를 여러 관심 서비스에 fan-out여러 subscriber가 같은 event를 각각 처리
Event Streamevent 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 couplingproducer와 consumer가 직접 연결되지 않음
비동기 처리producer가 consumer 처리를 기다리지 않아도 됨
확장성consumer를 독립적으로 scale 가능
실시간 반응event 발생 즉시 downstream 처리 가능
플러그인식 확장새로운 consumer 추가가 쉬움
장애 격리일부 consumer 장애가 producer를 직접 막지 않음
streaming analyticsevent 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이다.