ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 5장 - 스프링 데이터 JPA를 이용한 조회 기능
    📕 book/도메인 주도 개발 시작하기 2023. 1. 5. 00:26

    ❓CQRS

    💡 CQRS는 명령(Command) 모델과 조회(Query) 모델을 분리하는 패턴

     

    명령 모델 : 상태를 변경하는 기능 구현할 때 사용

    조회 모델 : 데이터를 조회하는 기능을 구현할 때 사용'

     

    엔티티, 애그리거트, 지포지터리 등 모델은 주문 취소, 배송지 변경과 같은 상태를 변경할 때 사용한다.

    즉, 도메인 모델은 명령 모델로 주로 사용된다.

     

    반면에 정렬, 페이징, 검색 조건 지정과 같은 기능은 주문 목록, 상품 상세와 같은 조회 기능에 사용된다.

     

    📌 검색을 위한 스펙


    검색 조건이 고정되어 있고 단순하다면 특정 조건을 조회하는 기능을 만들면 된다.

    public interface OrderDataDao {
    	
      Optional<OrderData> findById(OrderNo id);
      List<OrderData> findByOrderer(String ordererId, Data fromDate, Date toDate);
      ...
    }

    하지만, 목록 조회와 같은 기능은 다양한 검색 조건 을 조합해야 할 경우가 존재한다.

     

    스펙

    검색 조건을 다양하게 조합해야 할 때 사용할 수 있는 것이 스펙(specification)이다

     

    스펙은 애그리거트가 특정 조건을 충족하는지를 검사할 때 사용하는 인터페이스이다.

    public interface Specification<T> {
    
      public boolean isSatisfiedBy(T agg);
    	
    }
    • agg 파라미터는 검사 대상이 되는 객체이다.
    • 스펙을 리포지터리에 사용할 경우 → agg는 애그리거트 루트가 된다.
    • 스펙을 DAO에 사용할 경우 → agg는 검색 결과로 리턴할 데이터 객체가 된다.
    • 조건을 충족하면 true, 그렇지 않으면 false를 리턴

     

    ex) Order 애그리거트 객체가 특정 고객의 주문인지 확인하는 스펙

    public class OrdererSpec implements Specification<Order> {
    	
      private String ordererId;
    
      public OrdererSpec(String ordererId) {
        this.ordererId = ordererId;
      }
    
      public boolean isSatisfiedBy(Order agg) {
        return agg.getOrdererId().getMemberId().getId().equals(ordererId);
      }
    }

     

     

    ex) 리포지터리나 DAO는 검색 대상을 걸러내는 용도로 스펙을 사용

    public class MemberOrderRepository implements OrderRepository {
      
      public List<Order> findAll(Specification<Order> spec) {
        List<Order> allOrders = findAll();
        return allOrders.stream()
            .filter(order -> spec.isSatisfiedBy(order))
            .toList();
      }
    
    }
    
    // 리포지터리가 스펙을 이용해 검색 대상을 걸러준다.
    // 특정 조건을 충족하는 애그리거트를 찾고 싶다면 스펙을 생성해 리포지터리에 전달해주면 된다.
    Specification<Order> ordererSpec = new OrdererSpec("beomsic");
    // 리포지터리에 전달
    List<Order> orders = orderRepository.findAll(ordererSpec);

     

    📌 스프링 데이터 JPA를 이용한 스펙 구현


    스프링 데이터 JPA는 검색 조건을 표현하기 위한 인터페이스인 Specification 을 제공한다❗

     

    스프링 데이터 JPA가 제공하는 Specification 인터페이스

    public interface Specification<T> extends Serializable {
    
    	long serialVersionUID = 1L;
    
      ...
    	@Nullable
    	Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder);
    }
    • 제네릭 타입 파라미터 T - JPA 엔티티 타입을 의미한다.
    • toPredicate()
      • JPA Criteria API에서 조건을 표현하는 Predicate를 생성한다.

     

    스펙 인터페이스를 구현한 클래스 예시

    • 엔티티 - OrderSummary
    • ordererID 프로터티 값이 지정한 값과 동일하다.
    public class OrdererIdSpec implements Specification<OrderSummary> {
      
      private String ordererId;
      
      public OrdererIdSpec(String ordererId) {
        this.ordererId = ordererId;
      }
    
      @Override
      public Predicate toPredicate(Root<OrderSummary> root, CriteriaQuery<?> query,
          CriteriaBuilder criteriaBuilder) {
        return criteriaBuilder.equal(root.get(OrderSummary_.ordererId), ordererId);
      }
    }
    • 해당 클래스는 Specification<OrderSummary> 타입을 구현 ⇒ OrderSummary에 대한 검색 조건을 표현
    • toPredicate() 메서드를 구현
      • ordererId 프로퍼티 값이 생성자로 전달받은 ordererId와 동일한지 비교하는 predicate을 생성

     

    💡 JPA 정적 메타 모델

    OrdererIdSpec 클래스의 코드중에 OrderSummary_ 클래스가 있는데

    • 이는 JPA 정적 메타 모델을 정의한 코드이다.
    • 메타 모델 클래스는 모델 클래스의 이름 뒤에 ‘_’ 을 붙인 이름을 갖는다.
    • @StaticMetamodel 애너테이션을 이용해 관련 모델을 지정

    → 엔티티의 속성명까지 코드로 작성할 수 있다.

    정적 메타 모델 클래스는 대상 모델의 각 프로퍼티와 동일한 이름을 갖는 정적 필드를 정의한다.

     

    스펙 구현 클래스를 개별적으로 만들지 않고 별도 클래스에 스펙 생성 기능을 모아두어도 된다.

     

    스펙 생성 기능을 별도 클래스에 모음

    public class OrderSummarySpecs {
        public static Specification<OrderSummary> ordererId(String ordererId) {
            return (Root<OrderSummary> root, CriteriaQuery<?> query, CriteriaBuilder cb) ->
                    cb.equal(root.<String>get("ordererId"), ordererId);
        }
    
        public static Specification<OrderSummary> orderDateBetween(
                LocalDateTime from, LocalDateTime to) {
            return (Root<OrderSummary> root, CriteriaQuery<?> query, CriteriaBuilder cb) ->
                    cb.between(root.get(OrderSummary_.orderDate), from, to);
        }
    }
    
    // 사용
    Specification<OrderSummary> betweenSpec = OrderSummarySpecs.orderDateBetween(from, to);
    

    스펙 인터페이스는 함수형 인터페이스이다.

    • 따라서 람다식을 이용해 객체를 생성할 수 있다.

     

    📌 리포지터리 / DAO 에서 스펙 사용


    스펙을 만족하는 엔티티를 검색하고 싶다면 findAll() 메서드를 사용하면 된다.

    • findAll 메서드는 스펙 인터페이스를 파라미터로 갖는다.
    public interface OrderSummaryDao extends Repository<OrderSummary, String> {
      List<OrderSummary> findAll(Specification<OrderSummary> spec);
    }
    
    // 사용
    
    // 스펙 객체 생성
    Specification<OrderSummary> spec = new OrdererIdSpec("beomsic");
    // 검색
    List<OrderSummary> results = orderSummaryDao.findAll(spec);

    ⇒ OrderSummary에 대한 검색조건을 표현하는 스펙 인터페이스를 파라미터로 갖는다.

     

    📌 스펙 조합


    스프링 데이터 JPA가 제공하는 스펙 인터페이스는 스펙을 조합할 수 있는 두 메서드를 제공한다.

    • and
    • or

    두 메서드는 기본 구현을 가진 디폴트 메서드이다.

     

    and()

    두 스펙을 모두 충족하는 조건을 표현하는 스펙을 생성

    Specification<OrderSummary> spec = new OrdererIdSpec("beomsic")
          .and(OrderSummarySpecs.orderDateBetween(from, to));

     

    or()

    두 스펙 중 하나 이상 충족하는 조건을 표현하는 스펙을 생성

     

    not()

    • 정적메서드
    • 조건을 반대로 적용할 때 사용
    Specification<OrderSummary> spec = 
        Specification.not(OrderSummarySpecs.ordererId("beomsic"));

     

    where()

    • null 가능성이 있는 스펙을 다른 스펙과 조합할 때 사용하면 좋다.
      • null 을 전달하면 아무 조건도 생성하지 않는 스펙 객체를 리턴
      • null 이 아니면 인자로 받은 스펙 객체를 그대로 리턴
    Specification<OrderSummary> spec = 
        Specification.where(createNullableSpec()).and(createOtherSpec));

     

    📌 정렬 지정하기


    스프링 데이터 JPA는 두 가지 방법을 사용해 정렬을 지정할 수 있다.

    • 메서드 이름에 OrderBy를 사용해 정렬 기준 지정
    • Sort를 인자로 전달

     

    OrderBy

    특정 프로피터로 조회하는 find 메서드 → 이름뒤 OrderBy 를 사용해 정렬 순서를 지정할 수 있다.

    public interface OrderSummaryDao extends Repository<OrderSummary, String> {
      List<OrderSummary> findByOrdererIdOrderByNumberDesc(String ordererId);
    }

    장점

    • 사용하기 간단

     

    단점

    • 정렬 기준 프로퍼티가 두 개 이상이면 메서드 이름이 길어진다.
    • 메서드 이름으로 정렬 순서가 정해지기 때문에 상황에 따라 정렬 순서를 변경할 수 없다.

     

    Sort 타입

    스프링 데이터 JPA는 정렬 순서를 지정할 때 사용할 수 있는 Sort 타입을 제공한다.

    public interface OrderSummaryDao extends Repository<OrderSummary, String> {
      List<OrderSummary> findByOrdererId(String ordererId, Sort sort);
      List<OrderSummary> findAll(Specification<OrderSummary> spec, Sort sort);
    }
    
    // 사용
    Sort sort = Sort.by("number").ascending();
    List<OrderSummary> results = orderSummaryDao.findByOrdererId("beomsic", sort);

    find 메서드를 사용하는 코드는 알맞는 Sort 객체를 생성해 전달하면 된다.

     

    📌 페이징 처리하기


    스프링 데이터 JPA는 페이징 처리를 위해 Pageable 타입을 사용한다.

    public interface MemberDataDao extends Repository<MemberData, String> {
      List<MemberData> findByNameLike(String name, Pageable pageable);
    }

     

    Pageable 타입

    Pageable 타입 객체는 PageRequest 클래스를 이용해 생성한다.

    PageRequest pageReq = PageRequest.of(1, 10);
    List<MemberData> user = memberDataDao.findByNameLike("beomsic%", pageReq);
    • 한 페이지에 10개씩 표현, 두 번째 페이지 조회
    • 페이지 번호는 0번 부터 시작
    • 따라서 11 ~ 20 번째 데이터 조회

     

    PageRequest.of()

    • 첫 번째 인자 - 페이지 번호
    • 두 번째 인자 - 한 페이지의 개수

     

    PageRequest와 Sort를 사용하면 정렬 순서를 지정할 수도 있다.

    Sort sort = Sort.by("name").ascending();
    PageRequest pageReq = PageRequest.of(1, 10, sort);
    

     

    조건에 해당하는 전체 개수 구하기

    Pageable을 사용하는 메서드의 리턴 타입이 Page일 경우 스프링 데이터 JPA는 목록 조회 쿼리와 함께 COUNT 쿼리도 실행한다.

     

    📌 스펙 조합을 위한 스펙 빌더 클래스


    조건에 따른 스펙을 조합해야 하는 경우 if와 각 스펙을 조합하는 코드가 섞여 실수하기도 쉽고 복잡하다

    스펙 빌더를 통해 스펙을 조합한다.

    Specification<MemberData> spec = SpecBuilder.builder(MemberData.class)
         .ifTrue(searchRequest.isOnlyNotBlocked(),
                () -> MemberDataSpecs.nonBlocked())
         .ifHasText(searchRequest.getName(),
                name -> MemberDataSpecs.nameLike(searchReqeuest.getName()))
         .toSpec();
    • 코드 가독성을 높이고 구조가 단순해진다.

     

    📌 동적 인스턴스 생성


    JPA는 쿼리 결과에서 임의의 객체를 동적으로 생성할 수 있는 기능을 제공한다.

    public interface OrderSummaryDao extends Repository<OrderSummary, String> {
    
        @Query("""    
                select new com.myshop.order.query.dto.OrderView(
                    o.number, o.state, m.name, m.id, p.name
                )
                from Order o join o.orderLines ol, Member m, Product p
                where o.orderer.memberId.id = :ordererId
                and o.orderer.memberId.id = m.id
                and index(ol) = 0
                and ol.productId.id = p.id
                order by o.number.number desc
                """)
        List<OrderView> findOrderView(String ordererId);
    }
    • select 절에 new 키워드가 있다.
      • new 키워드 뒤에 생성할 인스턴스의 완전한 클래스 이름을 지정하고 괄호 안에 생성자에 인자로 전달할 값을 지정한다.

     

    장점

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

     

    📌 하이버네이트 @Subselect 사용


    @Subselect

    • 쿼리 결과를 @Entity로 매핑할 수 있는 기능❗
    @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;
        
      ...
      protected OrderSummary() {}
      ...
    }

    @Immutable, @Subselect, @Synchronize 는 하이버네이트 전용 애너테이션이다.

    • 이 태그를 사용하면 테이블이 아닌 쿼리 결과를 @Entity로 매핑할 수 있다.

     

    @Subselect

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

    • 쿼리 실행 결과를 매핑할 테이블 처럼 사용한다.

    @Subselect 로 조회한 @Entity는 수정할 수 없다.

    • 실수로 수정한다면 하이버네이트는 변경내역을 반영하는 update 쿼리를 실행할 것이다.
    • 이때, 매핑한 테이블이 없으므로 에러가 발생한다.

     

    @Immutable

    @Subselect 에서 발생하는 에러를 방지하기 위해 사용한다.

     

    이를 사용하면 해당 엔티티의 매핑 필드 / 프로퍼티가 변경되도 DB에 반영하지 않고 무시한다.

     

    @Synchronize

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

    이 코드는 Order의 상태를 변경하기 전에 OrderSummary를 조회한다.

     

    하이버네이트는 트랜잭션을 커밋하는 시점에 변경사항을 DB에 반영하므로 Order의 변경 내역을 아직 테이블에 반영하지 않은 상태에서의 테이블에서 OrderSummary를 조회한다.

    • 즉, 최신 값이 아니다.

     

    이런 문제를 해결하기 위해 @Synchronize 를 사용한다.

     

    @Synchronize는 해당 엔티티와 관련된 테이블 목록을 명시한다.

     

    하이버네이트는 엔티티를 로딩하기전 지정한 테이블과 관련된 변경이 발생하면 flush를 진행한다.

    • 따라서, OrderSummary를 로딩하는 시점에 변경 내역이 반영된다.

     

    참고자료

    http://www.yes24.com/Product/Goods/108431347

    댓글

Designed by Tistory.