https://github.com/Seungkyu-Han/micro_service_webflux
GitHub - Seungkyu-Han/micro_service_webflux: Webflux 환경에서 MSA의 Saga, Outbox, CQRS를 연습해보기 위한 리포지
Webflux 환경에서 MSA의 Saga, Outbox, CQRS를 연습해보기 위한 리포지토리입니다. - Seungkyu-Han/micro_service_webflux
github.com
기존의 Saga 패턴에서 더 강화된 내용이라고 생각하면 될 것이다.
Saga 패턴은 긴 트랜잭션을 짧은 트랜잭션으로 나누고 process와 rollback을 사용하여 하나씩 나아가는 구조였다.
여기서 이벤트를 전송하고, 변경된 내용을 데이터베이스에 저장하게 되는데 만약 이벤트를 전송하고 데이터베이스에서 에러가 발생해 이벤트만 전송되게 된다면 문제가 발생할 수 있다.
이런 문제를 해결하기 위해 사용하는 패턴이다.
방법은 먼저 데이터베이스에 변경사항들을 저장하고, 스케줄러를 사용해 한 번에 이벤트를 전송하는 것이다.
이 때 변경사항들은 기존의 데이터베이스가 아닌, 이벤트를 위한 별도의 저장공간을 만들게 된다.
이곳에 보낼 데이터를 미리 저장해두고 나중에 보내기 때문에 보낼 편지함(Outbox)패턴이라고 불리게 된다.
그리고 모든 사항이 완료된 Outbox 데이터들은 데이터베이스의 최적화를 위해 스케줄러를 사용해서 지속적으로 삭제해준다.
이러한 방법을 MSA에서 Outbox 패턴이라고 한다.
이렇게 별도의 데이터베이스에 저장해두고 한 번에 보내게되며, 실제 서비스에서는 1~2초의 간격으로 스케줄러를 실행한다고 한다.
우선 아래에는 직접 작성한 Outbox 패턴으로 설명해보겠다.
@Component
class PaymentOutboxScheduler(
private val paymentOutboxHelper: PaymentOutboxHelper,
private val paymentRequestMessagePublisher: PaymentRequestMessagePublisher
): OutboxScheduler {
private val logger = LoggerFactory.getLogger(PaymentOutboxScheduler::class.java)
@Transactional
@Scheduled(fixedDelay = 10000, initialDelay = 10000)
override fun processOutboxMessages() {
logger.info("결제를 요청하는 스케줄러가 실행되었습니다.")
paymentOutboxHelper.getPaymentOutboxMessageByOutboxStatusAndOrderStatus(
OutboxStatus.STARTED,
listOf(OrderStatus.PENDING, OrderStatus.CANCELLING)
).publishOn(Schedulers.boundedElastic()).map{
paymentOutboxMessage: PaymentOutboxMessage ->
if(paymentOutboxMessage.payload.orderStatus == OrderStatus.CANCELLING) {
paymentOutboxMessage.payload.paymentOrderStatus = PaymentOrderStatus.CANCELLING
}
paymentRequestMessagePublisher.publish(
paymentOutboxMessage = paymentOutboxMessage,
callback = ::updateOutboxStatus
).subscribe()
}.subscribe()
}
private fun updateOutboxStatus(paymentOutboxMessage: PaymentOutboxMessage, outboxStatus: OutboxStatus): Mono<Void> {
paymentOutboxMessage.outboxStatus = outboxStatus
return paymentOutboxHelper.save(paymentOutboxMessage).then()
}
}
주문 서버에서 결제 서버로 결제를 요청하는 과정이다.
우선 주문이 발생하면 Outbox의 상태가 Start인 값들만 조회한다.
처음에 Outbox에 Start로 저장을 하기 때문에 한번도 전송된 적이 없는 데이터를 불러오는 것이다.
그렇게 조회된 모든 데이터를 모두 publisher로 전송을 하며, 전송을 하면 callback을 사용해 Outbox의 Status를 Complete로 변경해준다.
publisher의 내용이다.
override fun publish(
paymentOutboxMessage: PaymentOutboxMessage,
callback: (PaymentOutboxMessage, OutboxStatus) -> Mono<Void>
): Mono<Void> {
return mono{
val paymentEventPayload = paymentOutboxMessage.payload
logger.info("{} 주문에 대한 이벤트 전송을 준비 중입니다.", paymentEventPayload.orderId.toString())
val paymentRequestAvroModel = paymentEventPayloadToPaymentRequestAvroModel(paymentEventPayload)
reactiveKafkaProducer.send(
paymentRequestTopic,
paymentEventPayload.orderId.toString(),
paymentRequestAvroModel
).publishOn(Schedulers.boundedElastic()).map{
callback(paymentOutboxMessage, OutboxStatus.COMPLETED).subscribe()
}.doOnError{
callback(paymentOutboxMessage, OutboxStatus.FAILED).subscribe()
}.subscribe()
logger.info("{}의 주문이 메시지 큐로 결제 요청을 위해 전송되었습니다", paymentEventPayload.orderId.toString())
}.then()
}
이렇게 해당 데이터를 model로 변환하여 kafka로 전송을 하고, 전송 상태의 여부에 따라 callback을 사용하여 outbox의 상태를 변환한다.
결제 서버의 내용은 작성하지 않겠지만, 결제 서버에서도 수신 받은 내용에 따라 내용을 처리하고 kafka로 전송할 데이터를 outbox에 저장해주면 된다.
이제 여기서 문제가 생기게 된다.
스케줄러를 사용하기 때문에 특정 시간에만 동기화가 이루어지게 되며, 해당 스케줄러가 동작하는 시간에만 CPU의 사용량이 늘어나게 된다.
이러한 방법을 해결하기 위해 마지막으로 CDC 패턴을 사용한다고 한다.
CDC와 관련된 내용은 CQRS 다음에 작성해보도록 하겠다.
'MSA' 카테고리의 다른 글
MSA에 CDC 적용을 위한 Debezium 도커 설정 (0) | 2025.03.02 |
---|---|
MSA에 CQRS 패턴 적용하기 (0) | 2025.03.01 |
MSA에 SAGA 패턴 적용하기 (0) | 2025.02.24 |
DDD에서 Hexagonal Architecture로 변경하기 (0) | 2025.02.20 |
Spring + Kafka에서 avro 사용하기 (1) | 2025.02.15 |