도메인 주도 설계 - 리포지토리의 조회 기능

2022. 5. 9. 21:29도메인주도설계

이 장에서 다룰 내용

  • 스펙
  • 동적 인스턴스와 @Subselect

검색을 위한 스펙

애그리거트를 찾을 때 식별자를 이용하는 것이 기본.

다양한 검색 조건으로 애그리거트를 찾아야한다면?

스펙

1
2
3
public interface Specification<T> {
  boolean isSatisfiedBy(T agg);
}
cs

 

스펙은 애그리거트가 특정 조건을 충족하는지 여부를 검사하는 것.

애그리거트가 조건에 맞으면 true를, 틀리면 false를 리턴합니다.

예) OrdererSpec

- 스펙 정의

1
2
3
4
5
6
7
8
9
10
11
12
public class OrdererSpec implements Specification<Order> {
    private String ordererId;
 
    public OrdererSpec(String ordererId) {
        this.ordererId = ordererId;
    }
 
    @Override
    public boolean isSatisfiedBy(Order agg) {
        return agg.getOrdererId().getMemberId().getId().equals(ordererId);
    }
}
cs

- 사용

1
2
3
4
5
6
7
8
public class MemoryOrderRepository implements OrderRepository {
    public List<Order> findAll(Specification spec){
        List<Order> allOrders = findAll();
        return allOrders.stream()
                .filter(order -> spec.isSatisfiedBy(order))
                .collect(toList());
    }
}
cs

원하는 스펙을 생성해서 레포지터리에 전달해 주기만 하면 됩니다.

스펙 조합

두 스펙을 AND 연산자나 OR 연산자로 조합해서 새로운 스펙을 만들 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class AndSpecification<T> implements Specification<T> {
    private List<Specification<T>> specs;
 
    public AndSpecification(Specification<T> ... specs) {
        this.specs = Arrays.asList(specs);
    }
 
    @Override
    public boolean isSatisfiedBy(T agg) {
        for (Specification<T> spec : specs)
        {
            if (!spec.isSatisfiedBy(agg)) return false;
        }
        return true;
    }
}
cs

JPA를 위한 스펙 구현

  • 위에서 보여준 방식은 성능에 아주 큰 문제가 있습니다.
  • 애그리거트가 10만개인 경우, 10만개의 데이터를 DB에서 메모리로 로딩한 뒤에 다시 10만 개 객체를 루프를 돌면서 스펙 검사를 하게 됩니다.
  • 따라서 실제 구현에서는 메모리에서 걸러내는 방식이 아닌 쿼리의 where를 사용하는 방식으로 바꿔야합니다.
  • JPA에서는 CriteriaBuilder를 사용합니다.

JPA 스펙 구현

1
2
3
4
5
6
7
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
 
public interface Specification<T> {
    Predicate toPredicate(Root<T> root, CriteriaBuilder cb);
}
cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class OrdererSpec implements Specification<Order> {
    private String ordererId;
 
    public OrdererSpec(String ordererId) {
        this.ordererId = ordererId;
    }
 
    @Override
    public Predicate toPredicate(Root<Order> root, CriteriaBuilder cb) {
        return cb.equal(root.get(Order_.orderer)
                            .get(Orderer_.memberId).get(MemberId_.id),
                ordererId);
 
    }
}
cs

 

조회 전용 기능 구현

레포지터리는 애그리거트의 저장소를 표현하는 것으로서 다음 용도로 레포지터리를 사용하는 것은 적합하지 않습니다.

  • 여러 애그리거트를 조합해서 한 화면에 보여주는 데이터 제공
  • 각종 통계 데이터 제공

여러 애그리거트를 조회해서 조합하는 것은 쉽지 않습니다.

통계 같은 경우 DBMS의 전용 기능을 사용해야 하기 때문에 JPQL이나 Criteria로 처리하기 힘듭니다.

 

이런 기능은 조회 전용 쿼리로 처리하면 됩니다.

JPQL 동적 인스턴스 생성

예)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class OrderView {
 
    private Order order;
    private Member member;
    private Product product;
 
    public OrderView(Order order, Member member, Product product) {
        this.order = order;
        this.member = member;
        this.product = product;
    }
 
    public Order getOrder() {
        return order;
    }
 
    public Member getMember() {
        return member;
    }
 
    public Product getProduct() {
        return product;
    }
}
cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Repository
public class JpaOrderViewDao implements OrderViewDao {
    @PersistenceContext
    private EntityManager em;
 
    @Override
    public List<OrderView> selectByOrderer(String ordererId) {
        String selectQuery =
                "select new com.myshop.order.query.dto.OrderView(o, m, p) "+
                "from Order o join o.orderLines ol, Member m, Product p " +
                "where o.orderer.memberId.id = :ordererId "+
                "and o.orderer.memberId = m.id "+
                "and index(ol) = 0 " +
                "and ol.productId = p.id "+
                "order by o.number.number desc";
        TypedQuery<OrderView> query =
                em.createQuery(selectQuery, OrderView.class);
        query.setParameter("ordererId", ordererId);
        return query.getResultList();
    }
}
cs

new 키워드 뒤에 생성할 인스턴스의 완전한 클래스 이름을 지정하고 괄호 안에 생성자에 인자로 전달할 값을 지정합니다.

동적 인스턴스의 장점은 JPQL을 그래도 사용하므로 객체 기준으로 쿼리를 작성하면서도 동시에 지연/즉시 로딩과 같은 고민 없이 원하는 모습으로 데이터를 조회할 수 있다는 점입니다.

하이버네이트 @Subselect 사용

예)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import org.hibernate.annotations.Immutable;
import org.hibernate.annotations.Subselect;
import org.hibernate.annotations.Synchronize;
 
import javax.persistence.*;
import java.util.Date;
 
@Entity
@Immutable
@Subselect("select o.order_number as number, " +
        "o.version, " +
        "o.orderer_id, " +
        "o.orderer_name, " +
        "o.total_amounts, " +
        "o.receiver_name, " +
        "o.state, " +
        "o.order_date, " +
        "p.product_id, " +
        "p.name as product_name " +
        "from purchase_order o inner join order_line ol " +
        "    on o.order_number = ol.order_number " +
        "    cross join product p " +
        "where " +
        "ol.line_idx = 0 " +
        "and ol.product_id = p.product_id"
)
@Synchronize({"purchase_order""order_line""product"})
public class OrderSummary {
    @Id
    private String number;
    private long version;
    private String ordererId;
    private String ordererName;
    private int totalAmounts;
    private String receiverName;
    private String state;
    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "orderDate")
    private Date orderDate;
    private String productId;
    private String productName;
 
    protected OrderSummary() {
    }
 
    ...
}
cs

 

@Subselect

  • 쿼리 결과를 @Entity로 매핑할 수 있습니다.
  • select 쿼리의 결과를 매핑할 테이블처럼 사용한다. 마치 DBMS의 뷰와 용도처럼 사용할 수 있다.
  • 이렇게 조회 전용 모델을 @Entity로 매핑하면 EntityManager의 find(), JPQL, Criteria, 스펙 등 @Entity 가 할 수 있는 기능들을 사용할 수 있다는 점이 장점입니다.
  • @Subselect의 값으로 지정한 쿼리를 from 절의 서브쿼리로 사용합니다. 서브쿼리를 사용하고 싶지 않다면 네이티비 SQL 쿼리를 사용하거나 Mybatis와 같은 별도 매퍼를 사용해서 조회 기능을 구현해야 합니다.

@Immutable

  • 뷰를 수정할 수 없듯이 @Subselect 로 조회한 @Entity 역시 수정할 수 없습니다.
  • 만약 @Subselect를 이용한 @Entity의 매핑 필드를 수정한다면 하이버네이트는 update 쿼리를 수행합니다.
  • 하지만 매핑한 테이블이 없으므로 에러가 발생합니다.
  • 이 때 @Immutable을 사용하면 필드가 변경되어도 DB에 반영하지 않고 무시하게 됩니다.

@Synchronize

1
2
3
4
5
6
// purchase_order 테이블에서 조회
Order order = orderRepository.findById(orderNumber);
order.chageShippingInfo(newInfo);
 
// 변경 내역이 DB에 반영되지 않아는데 purchase_order 테이블에서 조회
List<OrderSummary> summaries = orderSummaryRepository.findByIrderId(userId);
cs
  • 하이버네이트는 트랜잭션을 커밋하는 시점에 DB에 반영하므로, Order의 변경 내역을 아직 purchase_order 테이블에 반영하지 않은 상태에서 purchase_order 테이블을 사용하는 OrderSummary를 조회하게 됩니다.
  • 즉, OrderSummary에는 최신 값이 아닌 이전 값이 담기게 됩니다.
  • @Synchronize에 엔티티와 관련된 테이블 목록을 명시하면 하이버네이트는 엔티티를 로딩하기 전에 지정한 테이블과 관련된 변경이 발생하면 플러시를 먼저하게 됩니다.