반응형

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

 

MSA에서 제일 중요한 부분 중 하나라고 생각한다.

현재 개발 중인 서버의 구조이다.

 

  1. 클라이언트가 주문을 하면, 주문 서버의 API를 사용해 주문을 하게 된다.
  2. 주문 서버만을 사용해 주문을 완료할 수 없으니, 우선 고객의 잔액을 확인하기 위해 payment 서버로 메시지를 보내게 된다.
  3. 결제 서버는 금액과 잔액을 확인한 후에, 결제가 가능하다면 금액을 뺀 잔액을 저장하고 주문 서버로 응답 메시지를 보내게 된다.
  4. 주문 서버는 결제가 가능하다면, 식당 서버로 메시지를 보내 해당 식당의 상태와 메뉴를 확인한다.
  5. 식당 서버는 해당 메시지를 확인 한 후 그에 맞는 메시지를 주문 서버에게 응답한다.
  6. 해당 메시지를 확인 한 후, 결과를 데이터베이스에 저장한다.

 

이런 과정을 통해 결제가 진행된다.

늘 그렇듯 이런 과정에 transaction 처리를 해야한다.

하지만 다른 서버간에 메시지를 보내는 과정에서 어떻게 transaction 처리를 할 수 있을까?

이럴 때 사용하는 것이 saga 패턴이다.

 

Saga 패턴마이크로서비스 아키텍처에서 분산 트랜잭션을 관리하는 방법 중 하나다.

각 서비스가 개별적으로 트랜잭션을 수행하고, 트랜잭션 간 일관성을 유지하기 위해 보상 작업(rollback)이나 이벤트 체인을 활용하는 방식을 말한다.

 

즉 해당 서버로부터 응답을 받고, 그 결과에 따라 작업을 process 할지 rollback 할지 결정하는 것이다.

 

우선 현재 서비스를 분석해보면, 다음과 같은 실패가 존재한다.

  1. 결제 과정에서 잔액이 부족해 결제를 실패하는 경우
  2. 결제는 성공했지만, 식당에서 문제가 있어 주문이 실패하는 경우

1번은 잔액을 보존하고, 주문 서버로 실패 이벤트만 전송하면 되지만 2번은 아니다.

2번은 식당 서버에서 실패 이벤트를 받으면, 결제 서버로도 실패 이벤트를 전송해 잔액을 복구해야 한다.

 

우선 서비스에 적용을 해보도록 하자.

Saga의 적용을 위해 아래와 같은 인터페이스를 생성한다.

interface SagaStep<T, SuccessEvent: DomainEvent<*>, FailEvent: DomainEvent<*>> {

    fun process(data: T): Mono<SuccessEvent>
    fun rollback(data: T): Mono<FailEvent>
}

여기서 DomainEvent는 common 모듈에서 각각의 이벤트를 위해 상속받아 구현한다.

 

결제와 관련된 Saga는 다음과 같이 구현한다.

@Component
class OrderPaymentSaga(
    private val orderDomainService: OrderDomainService,
    private val orderRepository: OrderRepository
): SagaStep<PaymentResponse, OrderPaidEvent, EmptyEvent> {

    private val logger = LoggerFactory.getLogger(OrderPaymentSaga::class.java)

    override fun process(data: PaymentResponse): Mono<OrderPaidEvent> {
        logger.info("주문 {}의 상태를 결제완료로 변경합니다", data.id)
        return orderRepository.findById(ObjectId(data.id))
            .flatMap{
                val orderPaidEvent = orderDomainService.payOrder(order = it)
                orderRepository.save(it)
                    .thenReturn(orderPaidEvent)
            }.doOnNext{
                logger.info("주문 {}의 상태가 결제완료로 변경되어 저장되었습니다.", it.order.orderId.id)
            }
    }

    override fun rollback(data: PaymentResponse): Mono<EmptyEvent> {
        logger.info("주문 {}의 상태를 취소로 변경합니다.", data.id)
        return orderRepository.findById(ObjectId(data.id))
            .flatMap {
                orderDomainService.cancelOrder(order = it)
                orderRepository.save(it)
                    .thenReturn(EmptyEvent())
            }.doOnNext{
                logger.info("주문 {}의 상태가 취소로 변경되어 저장되었습니다.", data.id)
            }
    }
}

 

주문이 실패한다면, 해당 주문만 실패로 데이터베이스에 저장해준다.

주문이 성공한다면, 해당 주문을 결제성공으로 데이터베이스에 저장하고 식당 서버로 승인 요청 이벤트를 전송한다.

 

승인과 관련된 Saga이다.

@Component
class RestaurantApprovalSaga(
    private val orderDomainService: OrderDomainService,
    private val orderRepository: OrderRepository,
    private val orderCancelledPaymentRequestMessagePublisher: OrderCancelledPaymentRequestMessagePublisher
): SagaStep<RestaurantApprovalResponse, EmptyEvent, OrderCancelledEvent> {

    private val logger = LoggerFactory.getLogger(RestaurantApprovalSaga::class.java)

    @Transactional
    override fun process(data: RestaurantApprovalResponse): Mono<EmptyEvent> {
        logger.info("{} 주문이 승인 완료되었습니다", data.id)

        return orderRepository.findById(ObjectId(data.id))
            .flatMap {
                orderDomainService.approveOrder(order = it)
                logger.info("바뀐거: $it")
                orderRepository.save(it)
                    .thenReturn(EmptyEvent())
            }.doOnNext{
                logger.info("{} 주문이 승인 완료되어 저장되었습니다", data.id)
            }
    }

    @Transactional
    override fun rollback(data: RestaurantApprovalResponse): Mono<OrderCancelledEvent> {
        logger.info("{} 주문이 미승인 되었습니다", data.id)
        return orderRepository.findById(ObjectId(data.id))
            .flatMap {
                val orderCancelledEvent = orderDomainService.cancelOrderPayment(it)
                orderRepository.save(it)
                    .thenReturn(orderCancelledEvent)
            }.doOnNext{
                logger.info("{} 주문이 취소 중 상태로 저장되었습니다", it.order.orderId.id.toString())
            }.doOnNext{
                orderCancelledPaymentRequestMessagePublisher.publish(it)
            }.doOnNext {
                logger.info("{} 주문의 결제 취소 이벤트를 전송했습니다.", it.order.orderId.id.toString())
            }
    }
}

 

주문이 승인되었다면, 해당 주문만 승인 완료로 데이터베이스에 저장해주면 된다. (이미 결제 서버에서 결제는 완료되었기 때문에)

주문이 실패했다면, 결제 서버의 잔액을 복구시켜야 하기 때문에 결제서버로 실패 이벤트를 전송해준다.

 

이런 식으로 메시지를 받을 때마다, process 할건지 rollback 할건지 결정해서 다음 프로세스를 진행하면 된다.

그리고 당연히 메시지를 받고 보내는 과정까지의 해당 서버는 transaction 하게 동작해야 한다.

 

enum class OrderStatus {
    PENDING, PAID, APPROVED, CANCELLING, CANCELLED
}

 

주문의 상태는 다음과 같다.

PENDING: 주문이 막 생성된 상태

PAID: 결제가 완료된 상태 (Order -> Payment -> Order로 성공 이벤트를 응답받음)

APPROVED: 주문이 승인된 상태 (Order -> Payment -> Order -> Restaurant -> Order로 성공 이벤트를 응답받음)

CANCELLING: 승인이 취소된 상태 (Order -> Payment -> Order -> Restaurant -> Order로 실패 이벤트를 응답받음)

CANCELLED: 모든 취소가 완료된 상태

 

해당 상태들을 데이터베이스에 저장해가며, 장기 transaction을 메시지를 응답받고 보내는 과정으로 분해하여 transaction을 적용하며 saga패턴을 적용하면 된다.

 

해당 서버 내에서 transaction을 적용하는 것은 어렵지 않겠지만, 이런 프로세스를 이해 할 수 있도록 saga 패턴을 적용할 서버를 제대로 분석하는 것이 필요하다고 생각한다.

'MSA' 카테고리의 다른 글

MSA에 CQRS 패턴 적용하기  (0) 2025.03.01
MSA에 Outbox 패턴 적용하기  (0) 2025.02.28
DDD에서 Hexagonal Architecture로 변경하기  (0) 2025.02.20
Spring + Kafka에서 avro 사용하기  (1) 2025.02.15
DDD의 핵심 요소  (0) 2025.02.14
반응형

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

기존에 사용하던 Architecture는 보통 DDD였다.

물론 DDD도 굉장히 좋은 설계이지만, 다음과 같은 문제가 있었다.

 

 

보통 이렇게 개발이 되는데, 여기서 가장 핵심인 부분은 Domain Layer이다.

다른 Layer에 영향을 최대한 받지 않고, 독립적으로 존재할 수 있어야 가장 좋을 것이다.

하지만 지금은 Data Layer에 종속이 되어 있으며, Data Layer가 변경이 될 때마다 Domain Layer도 수정이 되어야 한다는 문제가 생긴다.

 

그렇기 때문에 이런 구조를 다음과 같이 변경해보려고 한다.

Domain Layer에서 사용할 Data Layer의 인터페이스만 만들어두고, 해당 인터페이스는 Data Layer에서 하는 것이다.

Domain Layer에서 사용하는 인터페이스를 포트라고 하고, 해당 인터페이스를 구현하는 객체를 어뎁터라고 한다.

 

이렇게 Domain Layer에서 포트를 만들고, Data Layer에서 어뎁터로 연결하는 방식을 사용하여 Domain Layer의 독립성을 높이는 방법이 Hexagonal Architecture이다.

 

 

이런 방법으로 다른 Layer들이 Domain Layer에 의존하도록 한다.

 

예를 들어보자면, 결제와 관련된 서비스를 개발하고 있다.

 

여기서 가장 독립적인 모듈은 domain인 payment-domain이다.

이런 식으로 포트를 만든다.

여기서 포트는 입력으로 들어오는 포트와 나는 출력 포트가 있다.

 

입력으로 들어오는 포트는 domain layer에서 기존처럼 개발 할 수 있다.

어차피 상위 layer에서 해당 포트를 사용하는 구조이기 때문이다.

 

출력으로 나가는 포트는 여기서 구현하는 것이 아닌, 인터페이스만 만들어두고 해당 인터페이스를 사용해서 domain layer를 개발한다.

이런 식으로 인터페이스만 만들어 사용한다.

 

해당 인터페이스의 구현은 외부 persistence 모듈이다.

 

이런 식으로 어뎁터 클래스를 만들고

dependency injection을 통해 외부에서 구현한 repository를 주입해준다.

message와 관련한 kafka도 데이터베이스와 같다.

 

이런 식으로 설계한다면, domain layer는 어떤 모듈도 의존하지 않는 가장 독립적인 상태로 개발이 가능하다.

'MSA' 카테고리의 다른 글

MSA에 CQRS 패턴 적용하기  (0) 2025.03.01
MSA에 Outbox 패턴 적용하기  (0) 2025.02.28
MSA에 SAGA 패턴 적용하기  (0) 2025.02.24
Spring + Kafka에서 avro 사용하기  (1) 2025.02.15
DDD의 핵심 요소  (0) 2025.02.14
반응형

MSA에서 굉장히 많이 사용하는 Kafka를 사용하려고 한다.

 

kafka에서는 이벤트를 주고 받을 때, 넘기는 데이터를 class로 정의해야 하는데 이것을 avro를 사용해서 정의해보려고 한다.

 

avro를 사용해서 resource 폴더에 json으로 포멧을 작성하고, avro를 실행하면 java의 클래스로 파일들이 생성되게 된다.

 

우선 gradle에 avro를 추가하자

 

build.gradle.kts를 사용했다.

import com.github.davidmc24.gradle.plugin.avro.GenerateAvroJavaTask

plugins {
    kotlin("jvm") version "1.9.25"
    id("com.github.davidmc24.gradle.plugin.avro") version "1.9.1"
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.apache.avro:avro:1.12.0")
}

tasks.test {
    useJUnitPlatform()
}

kotlin {
    jvmToolchain(17)
}

avro {
    setCreateSetters(false)
}

val generateAvro:TaskProvider<GenerateAvroJavaTask> = tasks.register("generateAvro", GenerateAvroJavaTask::class.java) {
    source("src/main/resources/avro")
    setOutputDir(file("src/main/java"))
    stringType.set("String")
    enableDecimalLogicalType = true
}

tasks.named("compileJava").configure {
    dependsOn(generateAvro)
}

 

source로 src/main/resources/avro를 지정해주었다.

해당 폴더에 avsc 파일을 생성한 후에 generateAvro를 gradlew로 실행해주면 된다.

 

avsc 파일의 예시이다.

{
    "namespace": "seungkyu.food.ordering.kafka.order.avro.model",
    "type": "record",
    "name": "PaymentRequestAvroModel",
    "fields": [
        {
            "name": "id",
            "type": {
                "type": "string",
                "logicalType": "uuid"
            }
        },
        {
            "name": "sagaId",
            "type": {
                "type": "string",
                "logicalType": "uuid"
            }
        },
        {
            "name": "customerId",
            "type": {
                "type": "string",
                "logicalType": "uuid"
            }
        },
        {
            "name": "orderId",
            "type": {
                "type": "string",
                "logicalType": "uuid"
            }
        },
        {
            "name": "price",
            "type": {
                "type": "long",
                "logicalType": "long"
            }
        },
        {
            "name": "createdAt",
            "type": {
                "type": "long",
                "logicalType": "timestamp-millis"
            }
        },
        {
            "name": "paymentOrderStatus",
            "type": {
                  "type": "enum",
                  "name": "PaymentOrderStatus",
                  "symbols": ["PENDING", "CANCELLED"]
               }
        }
    ]
}

 

우선 namespace는 생성할 파일의 위치이다.

name으로 해당 클래스의 이름을 지정해준다.

 

fields이다.

name으로 해당 클래스에서 파라미터의 이름을 지정해준다.

type으로 class에서 사용할 타입과, kafka에서 사용할 타입을 명시해준다.

 

이제 gradle로 실행해보자.

지정한 패키지에 해당 클래스들이 생성되었다.

 

들어가서 확인해보니, 이렇게 직접 수정하지 말라고 하는 안내도 있었다.

 

이렇게 생성한 class로 kafka 이벤트를 주고 받을 수 있게 되었다.

'MSA' 카테고리의 다른 글

MSA에 CQRS 패턴 적용하기  (0) 2025.03.01
MSA에 Outbox 패턴 적용하기  (0) 2025.02.28
MSA에 SAGA 패턴 적용하기  (0) 2025.02.24
DDD에서 Hexagonal Architecture로 변경하기  (0) 2025.02.20
DDD의 핵심 요소  (0) 2025.02.14

+ Recent posts