반응형

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
반응형

MSA를 알기 위해서는 우선 DDD를 먼저 알아야 한다고 한다.

DDD는 Domain Driven Design으로 도메인이라는 영역?, 집합?을 기준으로 아키텍처를 설계하는 것을 말한다.

 

DDD의 핵심 요소로는 Entity, Value Object, Aggregate, Aggregate Root, Domain Event, Domain Service, Application Service가 있으며 하나씩 알아보도록 할 것이다.

 

  • Entity

고유한 식별자로 구분되는 도메인 객체를 말한다.

객체의 Equal을 구분하는 방법은 파라미터, 참조를 확인하는 게 아니라 고유한 ID가 같은지 확인한다.

내부의 값들은 변경이 가능하며, 이 Entity 자체에 로직을 추가하여 사용한다.

data class User(
    val id: UUID,
    var name: String,
    var email: String
) {
    fun updateEmail(email: String) {
        this.email = email
    }
}

 

이런 식으로 id를 활용하여 동일성을 판단하고, updateEmail과 같은 함수들을 포함한다.

  • Value Object(값 객체)

값 자체로 동등성을 비교하는, id가 없는 객체이다.

값들을 전체 비교하여 동일한 객체로 간주한다.

내부의 값은 변경할 수 없으며, 필요하다면 객체를 새로 생성하여 사용해야 한다.

 

data class Address(
    val city: String,
    val street: String,
    val postalCode: String
)

 

이렇게 Id가 존재하지 않으며, parameter들만 동일하다면 동일한 객체이다.

 

  • Aggregate

관련된 Entity와 값 객체를 묶어서 일관성을 유지하는 군집을 말한다.

하나의 트랜잭션 단위이며, Aggregate Root를 사용해서만 외부 접근이 가능하다.

  • Aggregate Root

Aggregate 내의 최상위 Entity이며, Aggregate Root를 통해서만 Aggregate에 접근이 가능하다.

data class Order(
    val id: String,
    val customerId: String,
    var items: MutableList<OrderItem>,
    var status: OrderStatus
) {
    fun addItem(item: OrderItem) {
        items.add(item)
    }

    fun completeOrder() {
        status = OrderStatus.COMPLETED
    }
}

data class OrderItem(
    val productId: String,
    var quantity: Int,
    val price: Double
)

 

여기서 Order가 Aggregate Root, OrderItem이 Aggregate이며, OrderItem에 직접 접근하지 않고, Order의 메서드를 통해서만 접근하는 것을 볼 수 있다.

 

  • Domain Event

도메인 내에서 비즈니스적으로 중요한 이벤트를 명시하는 객체이다.

도메인 모델에서 비즈니스 이벤트를 명확하게 표현한다.

이벤트 발행 후 비동기로 데이터를 처리가능하다.

data class OrderCompletedEvent(
    val orderId: String,
    val completedAt: LocalDateTime
)

 

  • Domain Service

비즈니스 규칙을 처리하는 서비스이다.

Entity나 Value Object로 표현하기 어려운 비즈니스 로직을 담당하며, stateless 서비스이다.

class DiscountService {
    fun calculateDiscount(order: Order): Double {
        return if (order.items.size >= 5) 0.1 else 0.0
    }
}

 

Order에 넣기 애매한 비즈니스 규칙이기에 도메인 서비스로 분리한다.

도메인 엔티티와 상호작용하는 것을 볼 수 있다.

 

  • Application Service

비즈니스 흐름을 관리하는 서비스

유즈케이스를 중심으로 작업의 흐름을 제어하며, 도메인 서비스와 도메인 엔티티등을 호출해 작업을 조합한다.

상위 계층에서 외부 API 호출을 담당하며, 트랜잭션과 이벤트 발행의 책임이 있다.

@Service
class OrderService(
    private val orderRepository: OrderRepository,
    private val discountService: DiscountService
) {
    @Transactional
    fun createOrder(customerId: String, items: List<OrderItem>): Order {
        val order = Order(
            id = UUID.randomUUID().toString(),
            customerId = customerId,
            items = items.toMutableList(),
            status = OrderStatus.PENDING
        )
        // 할인 적용 (도메인 서비스 사용)
        val discount = discountService.calculateDiscount(order)
        if (discount > 0) {
            println("할인 적용: $discount")
        }

        // 주문 저장
        return orderRepository.save(order)
    }
}

 

도메인 서비스와 애플리케이션 서비스가 좀 헷갈리지만, 두 서비스는 호출하는 layer가 다른 것을 볼 수 있다.

'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
Spring + Kafka에서 avro 사용하기  (1) 2025.02.15

+ Recent posts