고딩왕 코범석

5장 - 스프링 데이터 JPA를 이용한 조회 기능 본문

Book Lounge/도메인 주도 개발 시작하기

5장 - 스프링 데이터 JPA를 이용한 조회 기능

고딩왕 코범석 2022. 7. 16. 20:56
반응형

Index

  1. CQRS
  2. 검색을 위한 스펙
  3. 스프링 데이터 JPA를 이용한 스펙 구현
  4. 정렬 지정
  5. 페이징 처리하기
  6. @Subselect

CQRS

  • 앞에서 봤던 애그리거트, 리포지터리, 엔티티와 같은 모델은 상태를 변경할 때 주로 사용한다.
  • 조회용 모델과 상태 변경의 모델은 다르기 때문에 만약 조회를 위해 엔티티에 연관 관계와 같은 설정이 변경될 수 있다면 가급적 조회 모델을 따로 만드는게 좋다.

위로

검색을 위한 스펙

  • 검색 조건을 조합해야 할 경우 조건별 find..()를 만들기 보다 애그리거트가 특정 조건을 충족하는지 검사할 때 사용하는 Specification 인터페이스를 만들어서 사용하는게 좋다.

    public interface Specification<T> {
        boolean isSatisfiedBy(T agg);
    }
  • isSatisfiedBy()의 파라미터는 검사 대상이 되는 객체다. 만약 이 스펙을 Repository에 사용하면 애그리거트 루트가 되고, DAO(조회를 위한 데이터 접근 객체)에 적용한다면 검색 결과로 사용할 데이터 객체가 된다.

  • 위 Specification 인터페이스를 구현해 XXXSpec 클래스를 만들어 조건에 맞는 엔티티 혹은 조회 모델을 찾는 Spec 클래스를 정의한다고 이해하면 된다.

  • 그리고 Repository 계층에서는 매개변수를 Specification 인터페이스를 받아 filter 조건으로 사용한다.

위로

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

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

    package org.springframework.data.jpa.domain;
    
    public interface Specification<T> extends Serializable {
        long serialVersionUID = 1L;
    
        static <T> Specification<T> not(@Nullable Specification<T> spec) {
            return spec == null ? (root, query, builder) -> {
                return null;
            } : (root, query, builder) -> {
                return builder.not(spec.toPredicate(root, query, builder));
            };
        }
    
        static <T> Specification<T> where(@Nullable Specification<T> spec) {
            return spec == null ? (root, query, builder) -> {
                return null;
            } : spec;
        }
    
        default Specification<T> and(@Nullable Specification<T> other) {
            return SpecificationComposition.composed(this, other, CriteriaBuilder::and);
        }
    
        default Specification<T> or(@Nullable Specification<T> other) {
            return SpecificationComposition.composed(this, other, CriteriaBuilder::or);
        }
    
        @Nullable
        Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder);
    }
  • 책에서는 Criteria가 나와있어 별도로 살펴보지는 않았다. 나도 그렇고 요즘은 기업에서 JPA를 사용한다면 Querydsl도 사용하기 때문에 이번 포스팅에서는 이런게 있다 정도로만 이해하고 넘어갔다.

위로

정렬 지정

  • 스프링 데이터 JPA는 다음과 같은 두 가지 방법을 사용해 정렬을 지정할 수 있다.
    • 메서드 명에 OrderBy를 사용해 정렬 기준 지정
    • Sort를 인자로 전달
  • 메서드 명을 통해 정렬하는 방법이 간단하지만 정렬 조건이나 다른 조건이 붙어버리면 다음과 같은 단점이 있다.
    • 메서드명이 너무 길어져서 가독성 저하
    • 메서드 이름으로 정렬 순서가 정해지기 때문에 상황에 따라 정렬 순서가 변경될텐데 이 부분에 있어 유연하지 못하다.
  • 이럴 때는 Sort 타입을 이용해 정렬한다.

위로

페이징 처리하기

  • JPA에서 페이징 처리는 Pageable을 통해 처리한다.
  • Pageable을 사용하는 메서드의 리턴 타입이 Page일 경우 JPA에서는 count 쿼리도 발생한다. 무한 스크롤의 경우 전체 컨텐츠에 대한 count 쿼리를 발생시킬 이유는 없으니 Slice 타입으로 리턴하는 것도 좋은 방법이다.

위로

@Subselect

  • @Subselect는 쿼리 결과를 @Entity로 매핑할 수 있는 유용한 기능이다.

  • @Subselect를 통해 조회 쿼리 자체를 값으로 가지며 하이버네이트는 select 쿼리의 결과를 매핑할 테이블처럼 사용한다.

  • 하지만, @Entity는 변경 감지 기능이 동작하기 때문에 해당 모델의 수정을 막기위해 @Immutable을 사용한다.

    @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;
      @Column(name = "orderer_id")
      private String ordererId;
      @Column(name = "orderer_name")
      private String ordererName;
      @Column(name = "total_amounts")
      private int totalAmounts;
      @Column(name = "receiver_name")
      private String receiverName;
      private String state;
      @Column(name = "order_date")
      private LocalDateTime orderDate;
      @Column(name = "product_id")
      private String productId;
      @Column(name = "product_name")
      private String productName;
    
        // constructor, getter
    }
  • 트랜잭션 문제와 관련한 아래 코드를 살펴보자.

    // Order 엔티티를 찾아 무언가 변경
    Order order = orderRepository.findById(orderId).orElseThrow();
    order.change();
    
    // 한 트랜잭션에 있어서 변경내역이 저장되지 않은 상태에서 조회 모델을 통해 조회
    List<OrderDataDto> datas = orderDataRepository.findByUserId(userId);
  • 위 코드에서는 order 엔티티에 대한 변경이 저장되지 않은 상태에서 조회 모델을 통해 조회한다.

  • 이렇게 되면 변경한 order 정보가 조회 전용 모델에 적용되지 않는 이슈가 발생하는데, 이 이슈를 @Synchronize로 해결한다.

    • @Synchronize는 해당 엔티티와 관련한 테이블을 기재하고 조회 모델을 가져오기 전 기재한 테이블에 변경이 발생하면 관련 내역을 먼저 flush() 후 조회한다.

주의사항

  • @Subselect는 명시된 쿼리를 from절의 서브쿼리로 실행시킨다. 이러한 형태의 쿼리가 발생된다는 점을 유의하고 사용하자.

위로

반응형