Lined Notebook

[DDD] Repository의 조회 기능(JPA 중심)

by HeshAlgo

DDD

1. 검증을 위한 스펙

- 식별자 외에 여러 조건을 이용해 애그리거트를 찾는 경우

public interface OrderRepository {
    Order findById(OrderNo id);
    List<Order> findByOrderer(String ordererId, Date fromDate, Date toDate);
    ...
}

-> 검색 조건의 조합이 다양해지면 너무 많은 메서드가 존재합니다.

 

이런 경우, 스펙(Specification)을 이용해 해결해나가야 합니다.

// 스펙 인터페이스
public interface Speficiation<T> {
    public boolean isSatisfiedBy(T agg);
}

agg -> 검사 대상이 되는 애그리거트 객체

isSatisfiedBy() -> 검사 대상객체가 충족하면 true, 그렇지 않으면 false

 

example

// 특정 고객의 주문인지 확인하는 스펙
public class OrdererSpec implements Specification<Order> {
    private String ordererId;
    public OrdererSpec(String orderId) {
        this.ordererId = ordererId;
    }
    public boolean isSatisfiedBy(Order agg) {
        return agg.getOrdererId().getMemberId().getId().equals(ordererId);
    }
}

 

 

2. 스펙 장점

- 두 스펙을 AND연산자나 OR 연산자로 조합해 새로운 스펙을 만들고 더 복잡한 조합으로 스펙을 만들 수 있다.

public class AndSpec<T> implements Specification<T> {
    private List<Specification<T>> specs;
    
    public AndSpecification(Specification<T> ... specs) {
        this.specs = Arrays.asList(specs);
    }
    
    public boolean is SatisfiedBy(T agg) {
        for (Specification<T> spec : specs) {
            if (!spec.isSatisfiedBy(agg)) return false;
        }
        return true;
    }
}

 

 

// Repository
Specification<Order> ordererSpec = new OrdererSpec("mavirus");
Specification<Order> orderDateSpec = new OrderDateSpec(fromDate, toDate);
AndSpec<T> spec = new AndSpec(orderSpec, orderDateSpec);
List<Order> orders = orderRepository.findAll(spec);

-> 다양한 검색 조건을 스펙으로 이용 가능

 

3. JPA 스펙 구현

- 지금까지의 내용은 모든 애그리거트를 조회한 다음에 스펙을 이용해 걸러내는 방식을 사용

- 실행 속도 문제가 발생

 

메모리에서 걸러내는 방식(X) -> 쿼리의 where 이용(O)

 

JPA를 위한 스펙에는 2가지가 존재

1) CriteriaBuilder

2) Predicate

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(Order_.memberId)
                            .get(MemberId_.id), ordererId);
    }
}

 

 

// Repository
Specification<Order> ordererSpec = new OrdererSpec("mavirus");
List<Order> orders = orderRepository.findAll(spec);

- Order의 orderer.memberId.Id 프로퍼티가 생성자로 전달받은 orderId와 같은지 비교하는 Predicate를 생성해서 리턴

 

 

4. 스펙을 사용하는 JPA 레포지터리 구현

public interface OrderRepository {
    public List<Order> findAll(Specification<Order> spec);
    ...
}
@Repository
public class JpaOrderRepository implements OrderRepository {
    @PersistenceContext
    private EntityManager entityManager;
    ...(다른 코드 생략)
    
    @Override
    public List<Order> findAll(Specification<Order> spec) {
        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
        CriteriaQuery<Order> criteriaQuery = cb.createQuery(Order.class);
        
        Root<Order> root = criteriaQuery.from(Order.class);    // 검색 조건 대상 루트 생성
        Predicate predicate = spec.toPredicate(root, cb);      // Predicate를 생성
        CriteriaQuery.where(predicate);                        // 쿼리의 조건으로 predicate를 전달
        
        criteriaQuery.orderBy(cb.desc(root.get(Order_.number).get(OrderNo_.number)));
        TypedQuery<Order> query = entityManager.createQuery(criteriaQuery);
        return query.getResultList();
    }
    
    
}

 

5. 조회 전용 기능 구현

다음 용도로 Repository를 구현하는 것은 적합하지 않다.

1) 여러 애그리거트를 조합해서 한 화면에 보여주는 데이터 제공

2) 각종 통계 데이터 제공

 

 Why?)

다양한 테이블을 조인하거나 DBMS 전용 기능을 사용해야 하는데 JPQL이나 Criteria로 처리하기 어려움

JPA의 지연 로딩, 즉시 로딩 설정, 연관 매핑

 

그렇기 때문에 위의 기능들은 조회 전용 쿼리로 처리해야 한다.

-> 동적 인스턴스 생성, @Subselect, 네이티브 쿼리를 이용해서 구현 가능

 

1) 동적 인스턴스 생성

@Repository
public class JpaOrderViewDao implementes OrderViewDao {
    @PersistenceContext
    private EntityManager em;
    
    @Override
    public List<OrderView> selectByOrderer(String oredererId) {
        String selectQuery = 
               "select new com.myshop.order.application.dto.OrderView(o, m, p) " +
               "from Order o join o.orderLines ol, Member m, Product p " +
               "where o.order.memberId.id = :ordererId " +
               "and o.orderer.memberId.id = m.id " +
               "and index(ol) = 0 " +
               "and ol.productId = p.id " +
               "order by o.number.number desc";
        TypedQuery<OrderView> query = em.createQuery(selectQuery, OverView.class);
        query.setParameter("ordererId", ordererId);
        return query.getResultList();
    }
    
}

-> OrderView 객체의 생성자에 인자로 전달하고 객체로부터 필요한 값을 추출

 

public class OrderView {
    private String number;
    private long totalAmounts;
    ...
    private String productName;
    
    public class OrderView(Order order, Member member, Product product) {
        this.number = order.getNumber().getNumber();
        this.totalAmounts = order.getTotalAmounts().getValue();
        ...
        this.productName = product.getName();
    }
    ... // get 메서드
}

- Presentation영역을 통해 사용자에게 데이터를 보여주기 위해 조회 전용 모델을 만든다.

- JPQL과 생성자를 수정하여 필요한 값만 전달 받아도 된다.

 

동적 인스턴스의 장점

-> 객체 기준으로 쿼리를 작성하면서도 지연/즉시 로딩과 같은 고민 없이 원하는 모습으로 데이터 조회 가능

 

2) 하이버네이트 @Subselect

- 하이버네이트는 JPA 확장 기능으로 @Subselect를 제공

@Entity
@Immutable
@Subselect("select o.order_number as number, " +
           "o.orderer_id, o.orderer_name, o.total_amounts, " +
           "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"
)

@Synchronuze({"purchase_order", "order_line", "product"})
public class OrderSummary {
    @Id
    private String number;
    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() {
    }
    ... // get 메서드
    
}

- @Immutable, @Subselect, @Synchronize는 하이버네이트 전용 어노테이션으로 쿼리 결과를 @Entity로 매핑

- @Subselect는 조회 쿼리를 값으로 갖는다.

- @Immutable은 변경 내용을 방지하기 위해 사용한다. (해당 엔티티의 필드/프로퍼티가 변경되어도 반영 무시)

- @Synchronize는 @Subselect가 만든 view에 접근하기 위한 테이블을 정의

 

@Synchronize

// 테이블 조회
Order order = orderRepository.findById(orderNumber);
order.changeShippingInfo(newInfo);
// 변경 내역이 DB에 반영 되지않음
List<OrderSummary> summaries = orderSummaryRepository.findByOrdererId(userId);

-> 하이버네이트는 변경사항을 트랜잭션을 commit하는 시점에 DB에 반영

하지만, 위 코드는 아직 변경내역을 반영하지 않은 상태

 

@Synchronize를 사용하면 엔티티를 로딩당하기 전에 지정한 테이블과 관련된 변경이 발생하면 플러시를 먼저한다.

즉, 해당 테이블에 변경사항이 발생하면 관련 내역을 먼저 플러시해 새로 로딩하는 시점에서는 변경 내역이 반영

'DDD' 카테고리의 다른 글

[DDD] Aggregate  (0) 2021.04.11
[DDD] Domain Driven Design 아키텍처  (0) 2021.04.11
[DDD] Domain Driven Design 개요  (0) 2021.04.06

블로그의 정보

꾸준히 공부하는 개발 노트

HeshAlgo

활동하기