관련 프로젝트 Github
개요
OrderService
와 DeliveryService
간의 통신을 Event
기반으로 전환하여 기존 동기 호출에서 발생하는 물리적 의존성을 제거하려고 한다.
현재 주문 생성은 사용자가 호출하는 API로, 응답 시간이 길어질 경우 사용자 편의성이 저하된다. 하지만 DeliveryService
는 주문 생성 과정에서 동기 방식으로 호출되며, 여러 서비스와 추가로 동기 통신을 수행해 배송 데이터를 생성한다. 이로 인해 배송 생성의 지연 시간이 사용자 응답 시간에 그대로 전파되고 있는 상황이다.
이를 개선하기 위해 메시지 브로커를 도입하여 두 서비스 간의 통신을 비동기 이벤트 기반으로 변경하고, 응답 시간 단축과 사용자 경험 향상을 도모하려 한다.
비동기 통신 방식 중 Event를 선택한 이유
Command
를 사용하지 않고 Event
를 사용한 이유는 확장성과 의존성 분리를 위함이다.
Mssage, Command와 Event의 차이는 아래의 포스팅에서 설명한다.
메시지 브로커 선택 (Kafka, RabbitMQ)
메시지 브로커로 사용되는 대표적인 두개의 기술이다.
Kafka
Kafka
를 선택한 이유는 메시지가 로그에 저장된다는 점, 각 소비자는 자신의 offset
을 기준으로 독립적으로 메시지를 읽을 수 있다는 점이다. 이런 특징은 EDD의 목적을 충족하기에 적합하다고 생각했다.
Kafka와 EDD를 접목시켰을 때, 발행자는 소비자를 알 필요가 없고, 새로운 소비자가 추가되더라도 메시지 발행 시스템은 영향을 받지 않도록 설계할 수 있다.
RabbitMQ
RabbitMQ
를 선택하지 않은 이유는 Queue의 특성을 강하게 띄고 있기 때문이다.
이벤트 기반 통신에 RabbitMQ가 적합하지 않은 이유는 아래와 같다.
Queue의 특성으로 인한 데이터 손실
- Queue로 들어온 메시지가 소모되면 기본적으로 Queue에서 삭제되는 방식으로 동작한다.
- 따라서, 하나의 이벤트가 여러 서비스에서 독립적으로 처리되어야 하는 경우(예:
OrderCreated
이벤트를 Product-service와 Delivery-service가 모두 처리해야 하는 경우), 한 서비스가 이벤트를 소비하면 다른 서비스는 이를 처리할 수 없게 된다.
여러 Queue 생성 및 관리의 복잡성
- 위의 문제를 해결하기 위해 RabbitMQ는 Exchange와 Binding 설정을 통해 Fanout 방식으로 각 서비스에 개별 Queue를 생성해줄 수 있다.
- 하지만 이는
OrderCreated
이벤트를 처리해야 하는 새로운 서비스가 추가될 때마다 order-service가 Exchange나 Queue를 관리하도록 수정해야 하는 문제를 초래하게 된다. - 새로운 서비스 추가 시 메시지 발행자(
order-service
)를 수정해야 하므로 서비스 간 의존성 분리라는 EDD의 핵심 이점이 훼손된다고 생각했다.
이벤트 재처리 및 복구의 제한
- RabbitMQ는 기본적으로 메시지를 삭제하므로, 이전 이벤트를 재처리하거나 특정 시점부터 다시 이벤트를 읽어야 하는 경우에 적합하지 않다고 한다.
- 반면 Kafka 같은 로그 기반 메시징 시스템은 메시지를 삭제하지 않고 로그에 저장하여 소비자가 필요한 시점에 다시 읽을 수 있다.
문제가 발생할 수 있는 시나리오
RabbitMQ는 우리가 익히 알고 있는 Queue의 역할을 한다. 즉 데이터를 pop하면 Queue의 데이터가 사라진다.
만약 OrderCreated 이벤트가 Queue에 발행되었고, Product, Delivery serivce가 이를 구독하여 이벤트에 따른 액션을 취해야 한다고 가정해보자.Product-service가 먼저 OrderCreated 이벤트를 pop해버리면 더 이상 Queue에 이벤트가 남아있지 않게 된다. 따라서 Delivery-serivce는 아무런 액션도 취할 수 없을 것이다.
이를 해결하기 위해선 OrderCreated 이벤트를 구독하는 각 서비스가 동일하게 데이터를 받을 수 있게 각각의 Queue를 생성해야 하고, 전송해줘야 할 것이다. 이는 결국 OrderCreated 이벤트를 구독하는 새로운 서비스가 생성되면, order-service를 수정해야 하는 상황을 초래한다.
이렇게 되면 EDD의 핵심 장점 중 하나인 서비스 간 의존성 분리가 제대로 이루어지지 않게 된다.
구현
아래와 같은 이벤트를 카프카에 발행한다.
public class DoaminEvent { }
public class OrderCreateEvent extends DomainEvent {
private final Long orderId;
private final Long productId;
private final Integer quantity;
private final Long orderedBy;
// ...
}
// kafka에 발행, 소비되는 객체
public class DomainEventEnvelop<T extends DomainEvent> {
private final T event;
// metadata
private final UUID eventId; // 이벤트 식별자
private final LocalDateTime createdAt; // 이벤트 생성 시간: 순서가 강제되는 이벤트를 위함
private final String eventType; // event 필드를 역직렬화 시 매핑해줄 클래스명
DomainEventEnvelop는 DomainEvent의 래퍼 클래스로,
Domain 데이터에 포함되지는 않지만 Event 처리에 필요한 메타데이터를 포함한다.
"Event" 와 "Event 처리 프로세스"는 서로 다른 관심사라고 판단했고 결합도를 낮추기 위해 위와 같이 설계했다.
현재 outbox pattern은 구현하지 않았고 도메인 데이터 영속화와 이벤트 발행을 하나의 트랜잭션으로 관리하도록 설계했습니다
결론
Kafka 와 RabbiMQ 는 메시지브로커라는 동일한 카테고리 내에 포함되지만 그 특성은 너무나도 다르다.
이번에는 EDD에 RabbitMQ가 왜 적합하지 않은지에 대해 조금 더 상세하게 다뤘다. 해당 내용을 바탕으로 Kafka가 EDD에 적합한 이유를 도출했고 적용시켰다.
“RabbitMQ를 사용한 EDD구현은 이벤트 구독 서비스를 추가할 때마다 메시지 발행자를 수정해야 하므로, 의존성 분리라는 EDD의 이점을 제대로 살리지 못한다” 고 판단했다.
(다른 이유는 본문에 있습니다.)
다음에는 RabbitMQ 를 적용하기 적합한 시나리오를 생각해보고 적용해보려 한다.
'오류 해결기' 카테고리의 다른 글
Jackson 직렬화 내부 동작 방식으로 인한 Redis 캐시 데이터 파싱 오류 (0) | 2024.12.16 |
---|---|
[Spring Security + JWT] 크롬과 Postman의 인증 결과가 다른현상 (0) | 2023.07.14 |
[Git] 작업이 종료 직전 브랜치를 잘못 생성한 걸 깨달았다,, (0) | 2023.06.30 |
[Git] PR을 메인 브랜치로 잘못 머지한 상황 (0) | 2023.06.30 |
[DB] JPA 무한참조 문제 (0) | 2023.03.18 |