IT recording...
[DDD START!] 5장 - 리포지터리의 조회 기능(JPA중심) 본문
https://adorable-aspen-d23.notion.site/DDD-START-5-JPA-8dba1d6e43d94aa2b9149dea10aa83b7
1. 검색을 위한 스펙
식별자 외에 다양한 조건으로 애그리거트를 찾으려면 어떻게 해야 할까?
⇒ specification(스펙) 사용!
- 스펙은 애그리거트가 특정 조건을 충족하는지 여부를 검사한다.
public interface Specifation<T>{
public boolean isSatisfiedBy(T agg);
}
→ agg(애그리거트 객체)가 조건을 충족하는지 boolean값을 리턴한다.
public class OrdereSpec implements Specification<Order> {
private String ordererId;
public OrdererSpec(String ordererId) {
this.ordererId = ordererId; //비교 대상
}
//Order 애그리거트 객체가 특정 고객의 주문인지 확인하는 스펙 구현
public boolean isSatisfiedBy(Order agg) {
return agg.getOrdererId().getMemberId().getId().equals(ordererId);
}
}
- 리포지터리는 스펙을 전달받아 애그리거트를 걸러내는 용도로 사용한다.
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());
}
}
- 특정 조건을 충족하는 애그리거트를 찾기 위해서는 스펙을 생성해서 리포지터리에 전달해주면 된다.
Specification<Order> ordererSpec = new OrdererSpec("madvirus"); //특정 스펙 생성
List<Order> orders = orderRepository.findAll(ordererSpec); //리포지터리에 전달
스펙 조합
- 두 스펙을 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 isSatisfiedBy(T agg) {
for (Specification<T> spec : specs){
if (!spec.isSatisfiedBy(agg)) return false;
}
return true;
}
}
Specification<Order> ordererSpec = new OrdererSpec("madvirus");
Specification<Order> orderDateSpec = new OrderDataSpec(fromDate,toDate);
AndSpec<T> spec = new AndSpec(ordererSpec,orderDateSpec); //여러 스펙 전달
List<Order> orders = orderRepository.findAll(spec); //and스펙을 리포지토리에 전달
- But, 앞선 리포지터리 코드는 모든 애그리거트 조회 후 스펙을 이용해서 걸러내는 방식이었다.
- 애그리거트가 10만 개인 경우 10만개 데이터를 DB에서 메모리로 로딩한 뒤 다시 10만 개 객체를 루프를 돌면서 스펙을 검사하게 되는데, 이는 성능 저하 문제를 일으킨다.
2. JPA를 위한 스펙 구현
- 실제 구현에서는 스펙을 쿼리의 where절을 사용하는 방식으로 바꿔야 한다.
JPA 스펙 구현
- CriteriaBuilder, Predicate를 사용한다.
public interface Specification<T> {
Predicate toPredicate(Root<T> root, CriteriaBuilder cb);
}
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); //Order 애그리거트의 orderer로 검색
}
}
Specification<Order> ordererSpec = new OrdererSpec("madvirus"); //스펙 생성
List<Order> orders = orderRepository.findAll(ordererSpec); //리포지터리 적용
- Specification 구현 클래스를 개별적으로 만들지 않고, 별도 클래스에 스펙 생성 기능을 모아도 된다.
public class OrderSpecs {
public static Specification<Order> orderer(String ordererId){
return (root,cb) -> cb.equal(
root.get(Order_.orderer).get(Orderer_.memberId).get(MemberId_.id),
ordererId);
}
public static Specification<Order> between(Date from, Date to) {
return (root, cb) -> cb.between(root.get(Order_.orderDate),from,to);
}
}
Specification<Order> betweenSpec = OrderSpecs.between(fromTime,toTime);
AND/OR 스펙 조합을 위한 JPA스펙 구현
- AND를 위한 JPA스펙
public class AndSpecification<T> implements Specification<T> {
private List<Specification<T>> specs;
public AndSpecificatoin(Specification<T> ... specs){
this.specs = Arrays.asList(specs);
}
@Override
public Predicate toPredicate(Root<T> root, CriteriaBuilder cb){
Predicate[] predicates = specs.stream()
.map(spec -> spec.toPredicate(root,cb))
.toArray(size -> new Predicate[size]); //predicate 배열 생성
return cb.and(predicates); //CriteriaBuilder를 사용하여 새로운 predicate 생성
}
}
- OR를 위한 JPA스펙
public class OrSpecification<T> implements Specification<T> {
private List<Specification<T>> specs;
public OrSpecification(Specification<T> ... specs) {
this.specs = Arrays.asList(specs);
}
@Override
public Predicate toPredicate(Root<T> root, CriteriaBuilder cb) {
Predicate[] predicates = specs.stream()
.map(spec -> spec.toPredicate(root,cb))
.toArray(Predicate[]::new); //predicate 배열 생성
return cb.or(predicates); //CriteriaBuilder를 사용하여 새로운 predicate 생성
}
}
- AND/OR 스펙을 생성해주는 팩토리 클래스
public class Specs{
public static <T> Specification<T> and(Specification<T> ... specs) {
return new AndSpecification<>(specs);
}
public static <T> Specification<T> or(Specification<T> ... specs) {
return new OrSpecification<>(specs);
}
}
//팩토리를 이용하여 스펙 조합하기
Specificatoin<Order> specs = Specs.and(
OrderSpecs.orderer("madvirus"), OrderSpecs.between(fromTime,toTime)
);
스펙을 사용하는 JPA 리포지터리 구현
public interfce 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); // spec으로 새로운 predicate 생성
**criteriaQuery.where(predicate); //조건 다 넣어놓기
criteriaQuery.orderBy(
cb.desc(root.get(Order_.number).get(OrderNo.number)));
TypedQuery<Order> query = entityManager.createQuery(criteriaQuery);**
return query.getResultList();
}
}
** 도메인 모델은 구현 기술에 의존하지 않아야 한다.
- 하지만, JPA 용 specification인터페이스는 JPA 의 Root와 CreateBuilder에 의존하고 있다.
- → 리포지터리 구현 기술에 의존하지 않는 Specification을 만들려면 많은 부분을 추상화 해야하는데, 이는 많은 노력을 요구한다.
- 따라서 한 어플리케이션에서 다양한 리포지터리 구현 기술을 사용하고 각 리포지터리에 대해 동일한 스펙 인터페이스를 사용해야 하는 경우만 스펙을 추상화하려는 노력을 해야 한다.
3. 정렬 구현
- JPA에서는 CriteriaQuery#orderBy()를 이용해서 정렬 순서를 지정한다.
- CriteriaBuilder#asc()와 desc()메서드로 정렬할 대상을 지정한다.
criteriaQuery.**orderBy**(
**cb.desc**(root.get(Order_.number).get(OrderNo.number)));//정렬할 대상 지정
- JPQL을 사용하는 경우에는 order by 절을 사용한다.
TypedQuery<Order> query = entityManager.createQuery(
"select o from Order o" +
"where o.orderer.memberId.id = :ordererId" +
**"order by o.number.number desc"**,
Order.class);
)
정렬 순서가 고정되지 않은 경우
- 정렬 순서를 응용 서비스에서 결정하는 경우에는 정렬 순서를 리포지터리에 전달할 수 있어야 한다.
- JPA Criteria는 Order타입을 이용해서 정렬 순서를 지정한다.
- 하지만 응용 서비스에서는 CriteriaBuilder에 접근할 수 없다.
- 따라서 JPA Order가 아닌 다른 타입을 이용해서 리포지터리에 정렬 순서를 전달한다.
- JPA리포지터리는 이를 다시 JPA Order로 변환한다.
- 문자열을 사용하여 정렬 순서 지정하기
List<Order> orders = orderRepository.findAll(somespec, "number.number")
@Repository
public class JpaOrderRepository implements OrderRepository{
..//생략
@Override
public List<Order> findAll(Specification<Order> spec, String ... orders){
..//생략
criteriaQuery.where(predicate);
**if(orders.length > 0) {
criteriaQuery.orderBy(JpaQueryUtils.toJpaOrders(root,cb,orders));
}**
TypedQuery<Order> query = entityManager.createQuery(criteriaQuery)
return query.getResultList();
}
}
public class **JpaQueryUtils**{
public static <T> **List<Order> toJpaOrders**(Root<T> root, CriteriaBuilder cb, String ... orders){
if (orders == null || orders.length == 0) return Collections.emptyList();
return Arrays.stream(orders)
.map(orderStr -> toJpaOrder(root,cb,orderStr)) //각각 toJpaOrder시행
.collect(toList());
}
private static <T> **Order toJpaOrder**(Root<T> root, CriteriaBuilder cb, String ... orders){
String[] orderClause = orderStr.split(" ");
boolean ascending = true;
if(orderClause.length == 2 && orderClause[1].equalsIgnoreCase("desc")) {
ascending = false;
}
String[] paths = orderClause[0].split("\\\\.");
Path<Object> path = root.get(paths[0]);
for (int i=1;i<paths.length;i++) {
path = path.get(paths[i]);
}
return ascending ? cb.asc(path) : cb.desc(path);
}
//JPQL의 order by 절 생성
public static **String** **toJPQLOrderBy**(String alias, String ... orders){
if(orders==null || orders.length == 0) return "";
String orderParts = Arrays.stream(orders)
.map(order -> alis + "." + order)
.collect(joining(", "));
return "order by " + orderParts;
}
}
⇒ 다음과 같이 사용 가능하다.
1. name desc → cb.desc(root.get(”name”))
2. customer.name asc → cb.asc(root.get(”customer”).get(”name”))
TypedQuery<Order> query = entityManager.createQuery(
"select o from Order o" +
"where o.orderer.memberId.id = :ordererId" +
**JpaQueryUtils.toJPQLOrderby("o", "number.number desc")**,
Order.class);
4. 페이징과 개수 구하기 구현
페이징
- JPA 쿼리에서는 setFirstResult() 와 setMaxResults() 메서드를 제공한다
- setFirstResult() : 읽어올 첫 번째 행 번호 지정 (0부터 시작)
- setMaxResults() : 읽어올 행 개수 지정
@Override
public List<Order> findByOrdererId(String ordererId, **int startRow, int fetchSize**){
TypedQuery<Order> query = entityManager.createQuery(
"select o from Order o "+
"where o.orderer.memberId.id = :ordererId" +
"order by o.number.number desc"),
Order.class);
query.setParameter("ordererId", ordererId);
**query.setFirstResult(startRow);
query.setMaxResults(fetchSize);**
return query.getResultList();
)
}
ex) 한 페이지에 15개 행이 보여지고, 4번째 페이지를 조회하려고 한다
→ 46번째 (0부터 시작하므로)
List<Order> orders = findByOrdererId("madvirus", 45, 15);
전체 개수 구하기
- JPQL을 이용한 구현
@Repository
public class JpaOrderRepository implements OrderRepository {
...
@Override
public Long countsAll() {
TypedQuery<Long> query = entityManager.createQuery(
**"select count(o) from Order o"**, Long.class);
return query.getSingleResult();
}
}
- specification 이용
- 특정 조건 충족하는 애그리거트 개수 구하기
@Repository
public class JpaOrderRepository implements OrderRepository {
...
@Override
public Long counts(Specification<Order> spec){
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<Long> criteriaQuery = cb.createQuery(Long.class); //리턴 클래스 지정
Root<Order> root = criteriaQuery.from(Order.class); //검사할 클래스 지정
**criteriaQuery.select(cb.count(root))
.where(spec.toPredicate(root,cb));**
TypedQuery<Long> query = entityManager.createQuery(criteriaQuery);
return query.getSingleResult();
}
}
- 지금까지 리포지터리의 스펙, 정렬 순서, 페이징을 위한 코드 구현에 대해 알아보았는데, 이러한 구현을 대부분 자동으로 해주는 모듈이 바로 JPA이다.
- JPA를 이용하여 인터페이스 작성만으로 구현할 수 있게 된다.
5. 조회 전용 기능 구현
- 리포지터리는 애그리거트의 저장소를 표현하는 것으로, 다음 용도로 사용하는 것은 적합하지 않다.
- 여러 애그리거트를 조합해서 한 화면에 보여주는 데이터 제공
- JPA의 지연 로딩과 즉시 로딩 설정, 연관 매핑 등으로 복잡해진다.
- 애그리거트 간에 직접 연관을 맺으면 ID 참조의 장점을 활용할 수 없게 된다.
- 각종 통계 데이터 제공
- 다양한 테이블을 조인하거나 DBMS 전용 기능을 사용해야 하는데, JPQL이나 Criteria로 처리하기 힘들다.
- 여러 애그리거트를 조합해서 한 화면에 보여주는 데이터 제공
⇒ 조회 전용 쿼리를 사용하자!
- 동적 인스턴스 생성
- 하이버네이트의 @Subselect 확장 기능
- 네이티브 쿼리 등
동적 인스턴스 생성
- JPA는 쿼리 결과에서 임의의 객체를 동적으로 생성할 수 있는 기능을 제공하고 있다.
@Repository
public class JpaOrderViewDao implements OrderViewDao{
@PersistenceContext
private EntityManager em;
@Override
public List<OrderView> selectByOrderer(String ordererId) {
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.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();
}
}
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 메서드
}
- 조회 전용 모델을 생성하는 이유(새로 추가한 밸류타입을 → 기본 타입으로 변환해준다.)
- : 표현 영역을 통해 사용자에게 데이터를 보여주기 위해 사용한다.
- 모든 객체를 전달하지 않고 필요한 개별 프로퍼티를 생성자에 전달해줘도 된다.
----JPQL
select new com.myshop.order.application.dto.OrderView(
o.number.number,o.totalAmounts,o.orderDate,m.id.id,m.name,p.name)
...생략
----
----자바생성자
public class OrderView {
private String number;
private Long totalAmounts;
...
private String productName;
public OrderView(String number, int totalAmounts, Date orderDate, String memberId,
String memberName, String productName) {
this.number = number;
this.totalAmounts = totalAmounts;
...
this.productName = productName;
}
...
}
- 동적 인스턴스의 장점
- JPQL을 그대로 사용하므로,
- 객체 기준으로 쿼리를 작성하면서도
- 동시에 지연/즉시 로딩과 같은 고민 없이 원하는 모습으로 데이터를 조회할 수 있다.
하이버네이트 @Subselect 사용
- @Subselect는 쿼리 결과를 @Entity로 매핑할 수 있는 유용한 기능이다.
@Entity
**@Immutable**
**@Subselect**("select o.order_number as number, " +
"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 String ordererId;
private String ordererName;
private int totalAmounts;
...
private String productName;
protected OrderSummary(){}
//..get 메서드
}
- @Subselect→ 하이버네이트는 이 select 쿼리의 결과를 매핑할 테이블처럼 사용한다.
- : 조회 쿼리를 값으로 갖는다.
- @Immutable
- subselect를 이용하여 조회한 entity를 수정할 수 없기 때문에 불변으로 설정한다.
- 해당 엔티티의 매핑 필드/프로퍼티가 변경되어도 DB에 반영하지 않고 무시한다.
- @Synchronize
- 해당 엔티티와 관련된 테이블 목록을 명시하여 엔티티 로딩 전, 지정한 테이블과 관련된 변경이 발생하면 플러시를 먼저 수행한다.
- Subselect 장점
- Entity와 같기 때문에 EntityManager#find(), JPQL, Criteria를 사용해서 조회할 수 있다.
- 스펙 또한 사용 가능하다
//@Subselct 를 적용한 @Entity는 일반 @Entity와 동일한 방법으로 조회할 수 있다.
OrderSummary summary = entityManager.find(OrderSummary.class, orderNumber);
TypedQuery<OrderSummary> query = em.createQuery("select os from OrderSummary "+
"os where os.ordererId = :ordererId" +
"order by os.orderDate desc", OrderSummary.class);
query.setParameter("ordererId", ordererId);
List<OrderSummary> result = query.getResultList();
'DevBookReview' 카테고리의 다른 글
[DDD START!] 4장 - 리포지터리와 모델 구현 (JPA중심) (0) | 2022.01.07 |
---|---|
[DDD START!] 3장 - 애그리거트 (0) | 2021.12.31 |
[DDD START!] 2장 - 아키텍처 (0) | 2021.12.28 |
[DDD START!] 1장 - 도메인 (0) | 2021.12.28 |
Comments