이전 포스팅에서 MSA(MicroService Architecture)에서 발생하는 문제점들을 EDA(Event Deriven Architecture)를 적용하여 해결할 수 있다고 말씀 드렸습니다.
EDA의 동작방식에 대해 알아보고, EDA를 적용해 MSA를 어떻게 개선하고 확장할 수 있을지 알아보도록 하겠습니다.

EDA의 동작 방식은 크게 이벤트 생성 → 이벤트 저장 → 이벤트 전달 → 이벤트 처리의 흐름으로 진행됩니다.
여기서 이벤트는 데이터의 생성, 변경, 삭제 등의 상태 변화라고 생각하시면 돼요.
이벤트는 데이터의 생성, 변경, 삭제 등의 상태 변화가 발생할 때 생성됩니다.
그럼 이 이벤트는 서비스의 이벤트 스토어라는 저장소에 보관이 됩니다.
이벤트가 발생하면 비동기 방식으로 다른 서비스에 전달되게 되는데요.
이 때 RabbitMQ, Kafka 등 메시지 큐 기술이 활용됩니다. Publish된 이벤트를 Subscribe하는 서비스들이 해당 메시지를 받아 처리하게 됩니다.
정리하면 다음과 같습니다.
1) 이벤트 생성
2) 이벤트 저장
3) 이벤트 전달
4) 이벤트 처리
MSA의 문제점
각 서비스마다 자신만의 데이터베이스를 가지고 있어서 데이터 일관성 문제가 발생할 수 있다고 말씀 드렸습니다.
기존 DBMS에서 데이터 일관성을 유지하기 위해 썼던 all commit or rollback 방식을 쓰지 못하기 때문에 데이터 일관성 문제가 발생을 하게 되는데요.
EDA를 적용하면 eventually consistency를 보장하는 방식으로 데이터 일관성 문제를 해결할 수 있습니다.
eventually consistency는 분산 시스템에서 데이터의 일관성이 즉시 보장되지는 않지만, 이벤트를 통해 모든 서비스가 동일한 데이터 상태로 수렴하는 방식이에요.
이제 EDA가 어떻게 eventually consistency를 보장할 수 있는지 알아보겠습니다.
MSA 서비스 초기에는 REST 통신을 통해 데이터 참조를 수행할 수 있으나, 서비스가 커지면 복잡성/성능 등의 사유로 서비스 간 데이터 반정규화가 발생할 수 있습니다. 이 때, EDA를 적용해 서비스 간 데이터를 동기화 할 수 있습니다. 예시를 들어 살펴보겠습니다.
MSA가 적용된 product, category로 구성된 백엔드가 있다고 가정해봅시다.
1) 성능 문제로 아래처럼 product의 데이터 베이스에 category의 데이터 categoryName 반정규화되어 있습니다.

2) category 데이터베이스의 name 필드의 shirts 데이터가 T-shirts 로 변경되었습니다. 이 때, 이벤트가 발행됩니다.

3) product에서 CategoryEdited 이벤트를 구독해 categoryName 필드에 변경사항을 반영하면 데이터가 동기화되어 데이터 일관성이 보장되게 됩니다.
기존 Monolithic System에서 문제 발생 시 일관된 commit 또는 rollback 처리가 가능합니다. 하지만 MSA 시스템에서는 서로 다른 서비스에 걸친 기능을 수행하는 도중 일관된 commit 또는 rollback을 수행할 수 없습니다. 이 때, EDA를 적용해 rollback 또는 retry를 처리할 수 있습니다. 예시를 통해 살펴보겠습니다.
시나리오: 온라인 쇼핑몰에서 주문 처리
주문 처리 흐름은 아래와 같습니다.
1) Order Service
OrderPlaced 이벤트를 발행합니다.2) Payment Service
OrderPlaced 이벤트를 구독하고 결제를 시도합니다.PaymentCompleted이벤트를 발행합니다.3) Inventory Service
PaymentCompleted 이벤트를 구독하고 재고를 차감합니다.InventoryUpdated 이벤트를 발행합니다.4) Shipping Service
InventoryUpdated 이벤트를 구독하고 배송을 준비합니다.OrderCompleted 이벤트를 발행합니다. Rollback이 필요한 경우 (결제 실패)
고객의 결제가 실패해서 롤백이 필요한 경우를 살펴볼게요.
PaymentFailed 이벤트를 발행합니다.PaymentFailed 이벤트를 구독하여 Rollback 합니다.| Service | 이벤트 트리거 및 처리 로직 |
|---|---|
| Order Service | - 고객이 상품 주문 시 OrderPlaced 이벤트 발행 - Order Service는 이벤트 로그 저장 - PaymentFailed 이벤트 구독 및 Rollback (OrderFailed 이벤트 로그 저장) |
| Payment Service | - OrderPlaced 이벤트 구독 및 결제 시도 - 성공 시 PaymentCompleted 이벤트를 발행 - 결제 실패 시 PaymentFailed 이벤트 발행 |
| Inventory Service | - PaymentCompleted 이벤트 구독 및 재고 차감 - 성공 시 InventoryUpdated 이벤트 발행 |
| Shipping Service | - InventoryUpdated 이벤트 구독 및 배송 준비 - 성공 시 OrderCompleted 이벤트 발행 |
이런 식으로 분산 시스템에서 특정 작업이 실패했을 때, 이전에 성공한 작업들을 되돌리기 위해 수행하는 트랜잭션을 보상 트랜잭션이라고 합니다. 분산 시스템에서는 ACID 트랜잭션을 보장하기 어렵기 때문에, 보상 트랜잭션을 통해 부분적으로 실패한 작업을 복구합니다.
Retry가 필요한 경우 (네트워크 문제)
이번엔 Retry가 필요한 경우를 생각해보겠습니다.
Inventory Service와 통신하는 과정에서 네트워크 문제가 발생하거나 Inventory Service가 응답하지 않는 경우를 가정해봅시다.
재고 차감 요청을 보냈으나, 네트워크 문제로 응답이 오지 않을 경우를 가정해봅시다.
이 때 메시지큐의 기능을 활용하여 재시도를 수행할 수 있습니다.
Message Queue의 requeue 또는 Dead Letter Queue(DLQ)를 활용합니다.
Retry 후에도 실패하면 Order Service에서 Rollback을 수행하여 주문을 취소합니다.
requeue는 Inventory Service가 응답하지 않으면 메시지를 다시 큐에 넣고 일정 시간 재시도 하는 방법이고
재시도를 했음에도 실패하면 해당 메시지는 DLQ로 이동해서 DLQ에 쌓인 메시지를 따로 모니터링 하거나 분석하거나 별도의 복구 프로세스를 수행할 수 있습니다.
1) 이벤트 설계
정상 시나리오에서 이벤트 흐름을 도출합니다. 장애 시나리오에서 장애 발생 시 Retry 또는 Rollback을 위한 이벤트 흐름을 도출 합니다.
2) 이벤트 스토어 설계
이벤트 스토어를 설계할 때 아래와 같은 요구사항들을 생각하여 설계해야 합니다.
3) Backing Service 선택
Backing Service는 이벤트 스토어를 구현하기 위한 기술 스택으로 NoSQL(DynamoDB, MongoDB), 관계형 DB(PostgreSQL), 메시지 큐(RabbitMQ, Kafka), 인메모리 데이터 그리드(Geode) 등이 있습니다. Backing Service를 통해 선처리 가능한 요구사항을 해결하고, 부족한 부분은 애플리케이션 레벨에서 구현합니다.
4) 보상 트랜잭션 (Compensating Transaction)
보상 트랜잭션이란 분산 시스템에서 특정 작업이 실패했을 때, 이전에 성공한 작업들을 되돌리기 위해 수행하는 트랜잭션입니다. 분산 시스템에서는 ACID 트랜잭션을 보장하기 어렵기 때문에, 보장 트랜잭션을 통해 부분적으로 실패한 작업을 복구합니다.
5) 비동기 통신
MSA에서는 서비스 간 데이터를 공유하지 않고, 비동기 통신에서는 요청과 응답이 동시에 이루어지지 않으므로, 사용자에게 즉각적인 응답을 제공하기 어렵습니다. 따라서 데이터를 미리 수집하는 등 데이터 수집 방식 설계에 대해 고려해야 합니다.
RESTful 통신 대신 EDA를 사용하면 synchronous 특성으로 인한 지연 및 리소스 점유 문제, 서비스 간 강한 의존성 문제 등을 해결할 수 있습니다.
예를 들어 "주문 완료" 이벤트를 발행할 때
OrderPlaced 이벤트를 메시지 큐(Kafka, RabbitMQ 등)에 발행합니다.| 이벤트 발행 서비스 | 발행하는 이벤트 | 구독하는 서비스 | 구독 후 수행 작업 |
|---|---|---|---|
| Order Service | OrderPlaced | Coupon Service | 쿠폰 할인 적용 후 CouponApplied 이벤트 발행 |
| Coupon Service | CouponApplied | Payment Service | 할인된 가격으로 결제 진행 |
| Payment Service | PaymentCompleted | Inventory Service | 재고 차감 후 InventoryUpdated 이벤트 발행 |
| Inventory Service | InventoryUpdated | Shipping Service | 배송 처리 |
따라서 새로운 서비스 추가 시 호출 흐름을 변경할 필요 없습니다. 그리고 서비스는 독립적으로 동작하고 강한 결합도가 사라집니다.
또한, 메시지 큐는 비동기 통신이기 때문에 동기 통신에서의 지연 발생, 리소스 부하, 장애 전파 같은 단점을 커버할 수 있습니다.
비동기 통신에서는 응답을 기다리지 않고 다른 요청을 처리할 수 있기 때문에 지연을 감소시키고 쓰레드가 낭비되는 등 리소스 부하를 감소시킬 수 있습니다.
또한 비동기 방식에서는 장애가 발생해도 전체 시스템이 멈추지 않고, 특정 요청만 실패하도록 설계할 수가 있습니다. Kafka 같은 메시지 큐를 사용하면, 장애가 발생해도 메시지를 계속 저장하고, 장애 복구 후 다시 처리할 수도 있습니다.
다만, EDA를 사용할 때는 다음과 같은 점들을 고려해야 합니다.
1. 메시지가 여러번 전달됨으로 인한 데이터 정합성 문제
OrderPlaced 이벤트를 발행OrderPlaced 이벤트가 두 번 전달됨이러한 문제를 해결하기 위해서는
1) Payment Service가 같은 주문 ID의 결제 요청이 여러 번 와도 한 번만 처리하도록 설계 하거나
2) Kafka의 트랜잭션 기능을 활용하여 카프카가 중복을 감지하고 하나의 이벤트를 저장하게 할 수 있습니다. 이벤트가 Exactly-Once 처리 되도록 보장해주는거죠.

2. 서비스 스케일링 문제
Inventory Service가 여러 개로 스케일링 되었을 경우, Order Service의 OrgerPlaced 이벤트를 동시에 가져가 같은 주문에 대해 재고가 여러번 차감될 가능성이 있습니다.
해결 방법으로는 다음과 같은 방법이 있습니다.
메시지 큐의 Consumer Group 기능 활용
Comsumer Group이란 동일한 서비스 인스턴스들을 하나의 논리적인 단위로 묶어놓은 것을 뜻합니다.

Kafka와 RabbitMQ는 하나의 메시지가 하나의 Consumer만 처리하도록 보장할 수 있습니다.
동일한 서비스 인스턴스들을 같은 Consumer Group으로 설정하면 각 메시지가 하나의 서비스 인스턴스에서만 처리됩니다.
데이터 정합성 체크
3. 장애 포인트
메시지큐 장애는 전체 서비스 장애로 이어질 수 있어 메시지 큐 장애 시 상황도 고려하여 설계를 해야 합니다.
OrderPlaced 이벤트를 받아야 하지만 메시지 큐(RabbitMQ, Kafka)가 다운됨.해결 방법으로는 다음과 같은 방법이 있습니다.
메시지 큐 장애를 대비해서
1) Kafka의 Replication 설정 고려
Kafka의 Replication 설정을 통해 메시지 큐 장애 시에도 다른 브로커가 메시지를 유지하도록 설정합니다.Kafka의 Replication 설정을 고려할 수 있습니다. Kafka에서는 메시지가 여러 Partition에 나뉘어 저장됩니다. Replication 설정을 통해 각 Partition을 여러 개의 브로커에 복제할 수 있습니다. 만약 Leader 브로커가 장애를 일으킬 경우, 자동으로 다른 Follower 브로커가 Leader로 승격되어 서비스 중단 없이 메시지를 처리할 수 있습니다.

2) RabbitMQ의 클러스터링 및 페일오버 설정 활용
RabbitMQ도 카프카 레플리케이션과 비슷하게 클러스터링과 페일오버를 설정하여 큐의 데이터를 여러 노드에 분산 저장하고, 장애가 발생하면 빠르게 복구할 수 있습니다.
3) Retry 및 Dead Letter Queue(DLQ) 활용
- Payment Service가 메시지를 받아 처리하다가 장애가 발생하면, 일정 시간 후 Retry하도록 메시지를 다시 큐에 넣습니다.
- 지정된 횟수만큼 실패하면 DLQ(Dead Letter Queue)로 이동하여 분석이 가능합니다.
4) 비즈니스 로직에서 보상 트랜잭션(Rollback) 고려
- 예를 들어, 결제가 성공했는데 재고 차감에서 실패하면 Payment Service가 PaymentRollback 이벤트를 발행하여 결제를 취소할 수 있습니다.

이벤트 기반 시스템에서는 이벤트를 생성하고 전달하는 과정이 반복적으로 수행됩니다.
정상 흐름
1) 서비스에서 상태 변경 발생
2) 이벤트 저장(Event Store 기록)
3) 이벤트 메시지가 Message Queue를 통해 전파됨
4) 관심 있는 서비스에서 이벤트를 구독(Consume)하여 비즈니스 로직 수행
오류 발생 시 처리 흐름
1) 결제 서비스에서 오류 발생
2) Fail 이벤트 발생
3) 이벤트 스토어를 통해 전파
4) 관심 있는 서비스에서 이벤트를 구독하여 롤백 수행
5) 이벤트 로그를 조회하여 롤백 수행
1) 사용자가 itemID: "C"를 장바구니에 추가 → cartItemAdded 이벤트 생성.
2) 장바구니에서 itemID: "C"를 제거 → cartItemRemoved 이벤트 생성.
3) 이러한 이벤트들은 Event Store에 기록되며, 장애 발생 시 이벤트 로그를 기반으로 장바구니의 특정 상태로 복원 가능.
https://medium.com/dtevangelist/event-driven-microservice-%EB%9E%80-54b4eaf7cc4a