MSA 환경에서 분산 트랜잭션 방식으로 구현 시 서비스 간 이벤트 발행이 필수이다.
각 서비스에서는 별도의 트랜잭션으로 로직을 처리하고 이벤트를 발행하게 되는데 이때 트랜잭션이 커밋 또는 롤백된 경우에만 이벤트가 발행되도록 보장해야 한다.
이 글에서는 Outbox 패턴과 Spring의 @TransactionalEventListener 방식을 고민한 끝에 @TransactionalEventListener를 사용하여 문제를 해결한 사례를 공유하고자 한다.
Outbox 패턴 vs. @TransactionalEventListener
MSA 환경에서는 서비스 간 데이터 일관성을 유지하기 위해 분산 트랜잭션 처리가 필요하다. 이때 트랜잭션 커밋 또는 롤백 시에만 카프카 이벤트가 발행되도록 보장해야 한다.
Outbox 패턴
Outbox 패턴은 데이터베이스의 Outbox 테이블에 이벤트를 저장하고, 트랜잭션 커밋 시 이를 카프카로 전송하는 방식이다.
장점
- 데이터 일관성 보장: 트랜잭션 커밋 시에만 이벤트가 발행되므로 데이터 일관성이 보장된다.
- 재전송 가능: Outbox 테이블에 이벤트가 저장되므로, 이벤트 발행 실패 시 재전송할 수 있다.
단점
- 복잡성: Outbox 테이블 관리와 이벤트 전송 로직 구현이 필요하여 복잡도가 높다.
@TransactionalEventListener
Spring에서 제공하는 @TransactionalEventListener 어노테이션은 트랜잭션 커밋 또는 롤백 시에만 이벤트를 발행할 수 있다. 이 방식은 별도의 Outbox 테이블 없이 간단하게 구현할 수 있다는 장점과 트랜잭션 커밋 즉시 이벤트를 발행할 수 있다는 장점이 있다.
장점
- 단순성: 별도의 Outbox 테이블 없이 간단하게 구현할 수 있다.
- 즉시성: 트랜잭션 커밋 시 즉시 이벤트가 발행된다.
단점
- 재전송 불가능: 이벤트 발행 실패 시 재전송할 수 없어 신뢰성이 저하될 수 있다.
@TransactionalEventListener 활용
단순하면서도 트랜잭션 커밋 시 즉시 이벤트가 발행 가능하다는 이유로 @TransactionalEventListener 방식을 선택했다.
이벤트 발행 리스너 구현
@Slf4j
@Component
public class TransactionEventListener {
private final KafkaSender kafkaSender;
public TransactionEventListener(KafkaSender kafkaSender) {
this.kafkaSender = kafkaSender;
}
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onOrderSuccess(ProcessOrderEvent event) {
try {
OrderRequestEventDto orderRequestEventDto = event.getOrderRequestEventDto();
log.info("traceId={}, 카프카 결제 요청 이벤트 발행", orderRequestEventDto.getTraceId());
kafkaSender.sendPaymentRequestEvent(KafkaVO.PAYMENT_REQUEST_TOPIC,
PaymentRequestEventDto.fromOrderRequest(orderRequestEventDto, event.getOrderId()));
} catch (Exception e) {
log.error("결제 요청 이벤트 발행 실패, 재시도 필요", e);
}
}
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onOrderStatusToCanceledSuccess(CancelOrderEvent event) {
try {
log.info("카프카 주문 취소 이벤트 발행");
kafkaSender.sendCancelOrderEvent(KafkaVO.CANCEL_ORDER_TOPIC,
new CancelOrderEventDto(event.getOrderId(), event.getGoodsId(), event.getQuantity()));
} catch (Exception e) {
log.error("주문 취소 이벤트 발행 실패, 재시도 필요", e);
}
}
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void onReturnSuccess(ReturnEvent event) {
try {
log.info("카프카 반품 완료 이벤트 발행");
kafkaSender.sendRollbackRequestEvent(KafkaVO.STOCK_ROLLBACK_TOPIC,
new StockRollbackEventDto(event.getGoodsId(), event.getQuantity(), null));
} catch (Exception e) {
log.error("반품 완료 이벤트 발행 실패, 재시도 필요", e);
}
}
}
트랜잭션 커밋/롤백 시 이벤트 발행/취소
- 트랜잭션 커밋 시: @TransactionalEventListener가 적용된 메서드가 호출되어 이벤트가 발행된다.
- 트랜잭션 롤백 시: @TransactionalEventListener가 적용된 메서드가 호출되지 않아 이벤트가 발행되지 않는다.
이처럼 @TransactionalEventListener를 사용하면 별도의 Outbox 테이블 관리 없이도 트랜잭션 커밋/롤백 시에만 이벤트가 발행/취소되도록 구현할 수 있다.
@TransactionalEventListener 신뢰성 저하 문제
이처럼 @TransactionalEventListener 방식은 단순성과 즉시성이 장점이지만, 위에서 언급했듯이 이벤트 발행 실패 시 재전송할 수 없어 신뢰성이 저하된다는 단점이 있다.
@TransactionalEventListener의 신뢰성 문제와 Kafka를 활용한 해결 방안
@TransactionalEventListener 방식은 구현이 간단하고 트랜잭션 결과에 따라 이벤트를 처리할 수 있다는 장점이 있지만, 이벤트 발행 실패 시 재발행 메커니즘이 없다는 단점이 있다.
하지만 Kafka를 이벤트 스트리밍 플랫폼으로 사용하여 다음과 같이 신뢰성을 보장하기에 괜찮을 것이라 판단했다.
1. 이벤트 발행 후 Kafka에 저장
트랜잭션이 커밋되면 @TransactionalEventListener가 즉시 이벤트를 발행하고, 이 이벤트를 Kafka 토픽에 저장한다. Kafka는 이벤트를 복제하여 브로커 장애 시에도 데이터를 안전하게 보관한다.
2. Kafka의 오프셋 관리 활용
Kafka 컨슈머는 오프셋을 관리하여 이벤트를 중복 없이 정확히 한 번씩 처리한다. 이를 통해 이벤트 처리의 일관성을 유지할 수 있다.
3. 장애 복구 시 이벤트 재처리
이벤트 발행 후 소비 과정에서 장애가 발생하면, Kafka에 저장된 이벤트는 그대로 유지된다. 장애가 복구되면 Kafka 컨슈머는 마지막으로 처리된 오프셋 이후의 이벤트를 다시 소비하여 이벤트 손실 없이 신뢰성을 보장할 수 있다.
이와 같은 방식으로 @TransactionalEventListener의 신뢰성 문제를 Kafka의 특성을 활용하여 해결할 수 있다. 이를 통해 트랜잭션 커밋 또는 롤백 시 즉시 이벤트를 발행하면서도 데이터 손실 없이 신뢰성을 유지할 수 있다.
이외에도 로그를 통해 원인을 파악하고 Kafka 메트릭에 대해 Prometheus나 Grafana와 같은 모니터링 툴을 사용해 문제에 대응할 수 있을 것으로 보인다.
또한 Kafka의 분산 아키텍처 특성을 살려 카프카 클러스터 내의 브로커 수를 늘려 고가용성과 부하 분산을 통해 신뢰성 저하 문제를 해결할 수 있을 것으로 보인다.
결론
MSA 환경에서 분산 트랜잭션을 처리할 때 Outbox 패턴과 @TransactionalEventListener 방식을 고민한 끝에, 단순성과 즉시성의 이유로 @TransactionalEventListener를 활용하여 문제를 해결할 수 있었다. 이를 통해 별도의 Outbox 테이블 관리 없이도 트랜잭션 커밋/롤백 시에만 이벤트가 발행/취소되도록 구현할 수 있었다.
'기술 블로그 > MiriMiri' 카테고리의 다른 글
비동기 통신을 통한 마이페이지 성능 개선 (0) | 2024.05.20 |
---|---|
MSA 환경에서 Kafka 이벤트 기반 주문 처리와 트랜잭션 관리 (0) | 2024.05.14 |
Redis 복제(Replication)를 사용한 상품 재고 관리 (0) | 2024.05.14 |
Kafka에서 동일한 토픽을 여러 서비스에서 소비하는 문제 해결하기 (0) | 2024.05.14 |
상품 주문 성능 개선 과정 (0) | 2024.05.10 |