반응형

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

개발을 하다보면 기록을 남겨야 하기 때문에, 로그 말고도 Soft delete를 사용할 때가 있다.

 

Soft delete는 데이터베이스에서 데이터를 delete 할 때 실제로 delete 쿼리를 날려 삭제하는 것이 아니라,

deleted와 같은 column의 값을 false에서 true로 바꿔주는 update 쿼리를 날려 실제 데이터는 남겨놓으면서 삭제되었다고 알려주는 방식을 말한다.

 

실제 비즈니스 개발을 할 때는, 사용자가 삭제된 데이터를 다시 요청하는 경우도 있기 때문에 대부분의 경우 Soft delete를 사용한다고 배웠다.

 

JPA에서도 이런 Soft delete 방식을 편리하게 지원한다.

 

물론 update를 하는 방식을 계속 사용할 수는 있지만, 그래도 편리하게 다음과 같은 기능을 제공하니 알아보도록 하자.

 

  • @SQLDelete

우선 삭제를 위한 @이다.

@SQLDelete를 사용하면 JPA에서 Delete를 사용할 때 실제로 삭제하는 것이 아니라, 해당 update 쿼리를 날려서 update하게 된다.

 

@SQLDelete(sql = "UPDATE assignment SET deleted = true where id = ?")

 

이런식으로 사용하면 된다.

 

이러면 deleted의 값을 true로 변경해주게 된다.

 

  • @SQLRestriction

조회를 위한 @이다.

@SQLRestriction을 사용하면 JPA에서 findBy를 사용할 때, 해당 column까지 확인을 해서 데이터를 가져오게 된다.

만약 해당 데이터가 이곳의 명시된 값과 다르면 조회하지 않는 것이다.

 

@SQLRestriction("deleted = false")

 

이런식으로 deleted는 false라는 값의 제약을 주었다.

 

보통은 이 2개를 같이 사용하며, 그 예시는 아래와 같다.

 

@Entity
@SQLRestriction("deleted = false")
@SQLDelete(sql = "UPDATE assignment SET deleted = true where id = ?")
data class Assignment(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Int?,

    @Column(length = 200)
    var title: String?,

    @Column(length = 500)
    var description: String?,

    var score: Int,

    var startDate: LocalDate,

    var endDate: LocalDate,

    var deleted: Boolean = false,
)

 

 

+ Recent posts