CQRS는 명령 모델과 조회 모델을 분리하는 패턴이다.
- 검색 조건이 고정되어 있고 단순하면 특정 조건으로 조회하는 기능을 만들 수 있다.
- 다양한 조건으로 조합해야될 떄마다 정의를
find
로 할 필요는 없다.
interface OrderDataDao {
fun findById(id: OrderNo): OrderData?
fun findByOrderer(ordererId: String, fromData: Date, toDate: Date): List<OrderData>
}
검색 조건을 다양하게 조합해야할 때 사용할 수 있는 것이 스펙이다. 스펙은 애그리거트가 특정 조건을 충족하는지를 검사할 때 사용하는 인터페이스다.
interface Specification<T> {
fun isSatisfiedBy(agg: T): Boolean
}
위 코드의 agg 파라미터는 검사 대상이 되는 객체다. 스펙을 레포지터리에 사용하면 agg는 애그리거트 루트가 되고, 스펙을 DAO에 사용하면 agg는 검색 결과로 리턴할 데이터 객체가 된다.
class OrderSpec(private var ordererId: String): Specification<Order> {
fun OrdererSpec(ordererId: String) {
this.ordererId = ordererId
}
override fun isSatisfiedBy(agg: Order): Boolean {
return agg.orderId.memberId.id.equals(ordererId)
}
}
레포지터리나 DAO는 검색 대상을 걸러내는 용도로 스펙을 사용한다. 만약 레포지터리가 메모리에 모든 애그리거트를 보관하고 있다면 다음과 같이 스펙을 사용할 수 있다.
class MemoryOrderRepository: OrderRepository {
fun findAll(spec: Specification<Order>): List<Order> {
val allOrders = findAll()
return allOrders.stream()
.filter { order -> spec.isSatisfiedBy(order)}
.toList()
}
}
특정 조건을 충족하는 애그리거트를 찾고 싶으면 원하는 스펙을 생성해서 레포지터리에 전달해 주기만 하면 된다.
실제 스펙은 이렇게 구현하지 않는다. 모든 애그리거트 객체를 메모리에 보관하기도 어렵고 설사 메모리에 보관할 수 있다 하더라도 조회 성능에 심각한 문제가 발생하기 떄문이다. 스펙은 사용하는 기술에 맞춰 구현하면 된다.
JPA에 있는 specification을 통해 구현한 spec은 아래의 코드와 같다.
class OrderIdSpec(private var ordererId: String): Specification<OrderSummary> {
fun OrdererSpec(ordererId: String) {
this.ordererId = ordererId
}
override fun toPredicate(
root: Root<Order>,
query: CriteriaQuery<*>,
criteriaBuilder: CriteriaBuilder
): Predicate? {
return criteriaBuilder.equal(root.get(OrderSummary.ordererId), ordererId)
}
}