[DDD] 5장. 스프링 데이터 JPA를 이용한 조회 기능

매빈·2023년 3월 25일
0

CQRS란, 명령 모델과 조회 모델을 분리하는 패턴.

  • 명령 모델: 상태를 변경하는 기능을 구현할 때 사용
  • 조회모델은 데이터를 조회하는 기능을 구현할 때 사용

1. 스펙


5.2 검색을 위한 스펙

  • 스펙
    • 애그리거트가 특정 조건을 충족하는지를 거사할 때 사용하는 인터페이스
    • 검색 조건을 다양하게 조합해야 할 때 사용
    • 정의
      public interface Speficiation<T> {
      	public boolean isSatistiedBy(T agg); 
          // agg 파라미터는 검사 대상이 되는 객체
          
      }
    • 스펙 ➡️ 리포지터리에 사용: agg = 애그리거트 루트
    • 스펙 ➡️ DAO: agg = 검색 결과로 리턴할 데이터 객체
    • 리포지터리나 DAO는 검색 대상을 걸러내는 용도로 스펙을 사용함

2. JPA 스펙 구현


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

  • 스프링 데이터 JPA가 제공하는 Spicification(스펙 인터페이스)
    // generic type parameter T = JPA 엔티티 타입
    public interface Specification<T> etends Serializable {
    	// not, where, and, or 메서드 생략
      
      @Nullable
      Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb);
    }

    ➕ JPA 정적 메타 모델

    • @StaticMetamodel 애너테이션을 이용하여 관련 모델을 지정
    • 모델 클래스의 이름 뒤에 _를 붙인 이름을 가진
    • 문자열로 프로퍼티를 지정할 수도 있음. 하지만 오타 가능성이 있고 코드 안정성이나 생산성 측면에서 정적 메타 모델 클래스가 유리함.
    • JPA 프로바이더가 정적 메타 모델을 생성하는 도구를 제공, 사용하면 편리함
  • 스펙 구현 클래스를 개별적으로 만들지 않고 별도 클래스에 스펙 생성 기능을 모아둘 수 있음.
  • 람다식을 이용하여 객체 생성할 수 있음.
    return (Root<OrderSummary> root, CriteriaQuery<?> Query, CriteriaBuilder ch) ->
    	cb.equal(root.<String>get("ordererId"), ordererId);

5.4 리포지터리/DAO에서 스펙 사용하기

  • 스펙을 충족하는 엔티티를 검색할 때 findAll() 메서드 사용
  • ex.
public interface OrderSummaryDao extands Repository<OrderSummary, String> {
	List<OrderSummary> findAll(Specification<OrderSummary> spec);
}
  • 스펙 구현체와 findAll 메서드를 이용하여 특정 조건을 충족하는 엔티티 검색하기
// 스펙 객체를 생성하고
Specification<OrderSummary> spec = new OrdererIdSpec("user1");

// findAll() 메서드를 이용하여 검색
List<OrderSummary> results = orderSummaryDao.findAll(spec);

5.5 스펙 조합

  • 스펙 인터페이스는 and, or 메서드를 제공, 이 메서드들을 이용하여 스펙을 조합할 수 있음.

    public interface Specification<T> extends Serializable {
        ...
    
        default Specification<T> and (@Nullable specification<T> other) {
        ...
        }
        default Specification<T> other) {
        ...
        }
    
        @Nullable
        Predicate toPredicate(Root<T> root, CriteriaQuery<?> query,
        CriteriaBuilder criteriaBuilder);
    }
  • not() 메서드도 사용할 수 있음. 정적 메서드로 조건을 반대로 적용할 때 사용

    Specification<OrderSummary> spec = Specification.not(OrderSummarySpecs.ordererId("userId"));
  • where() 메서드를 사용하여 NullPointerException 발생 방지하기

    Specification<OrderSummary> spec = Specification.where(createNullableSpec()).and(createOtherSpec());

3. 정렬과 페이징


5.6 정렬 지정하기

  • JPA가 정렬을 지정할 수 있음
    • 메서드 이름에 OrderBy를 사용하여 정렬 기준 지정
      public interface OrderSummaryDao extends REpository<OrderSummary, String> {
      	List<OrderSummary> findByOrdererIdOrderByNumberDesc(String ordererId);
          ...
          // ordererId 프로퍼티 값을 기준으로 검색 조건 지정
          // number 프로퍼티 값 역순으로 정렬
      }
      ➡️ 정렬 기준 프로퍼티가 두 개 이상이면 메서드 이름이 길어짐
    • 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();

5.7 페이징 처리하기

  • Pageable 타입 파라미터: 페이징을 자동으로 처리해줌
    public interface MemberDataDao extends Repository<MemberData, String> {
        // 마지막 메서드로 pageable 타입을 가짐.
        List<MemberData> findByNameLike(String name, Pageable pageable);
    }
  • findByNameLike() 메서드를 호출하는 예
    PageRequest pageReq = PageRequest.of(1, 10);
    List<MemberData> user = memberDataDao.findByNameLike("사용자%", pageReq);

    findBy프로퍼티 형식의 메서드는 Pageable 타입을 사용하더라도 리턴 타입이 ListCOUNT 쿼리를 실행하지 않으므로, 페이징 처리와 관련된 정보가 필요 없다면 Page 리턴 타입이 아닌 List를 사용하기.

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

  • 스펙 빌더: 코드 가독성을 높이고 구조를 단순화함
// if를 조합한 경우
Specification<MemberData> spec = Specification.where(null);
if (searchRequest.isOnlyNotBlocked()) {
	spec = spec.and(MemberDataSpecs.nonBlocked());
}
if (StringUtils.hasText(searchRequest.getName())) {
	spec = spec.and(MemberDataSpecs.nameLike(searchRequest.getName()));
}
List<MemberData> results = memberDataDao.findAll(spec, PageRequest.of(0.5));

// 스펙 빌더를 사용한 경우
Specification<MemberData> spec = SpecBuilder.builder(MemberData.class)
	.ifTrue(searchRequest.isOnlyNotBlocked(),
    () -> MemberDataSpecs.nonBlocked())
    .ifHasText(searchRequest.getName(),
    	name -> MemberDataSpecs.nameLike(searchRequest.getName())
    .toSpec();
List<MemberData> result = memberDataDao.findAll(spec, Pagerequest.of(0,5));

4. 동적 인스턴스와 @Subselect


5.9 동적 인스턴스 생성

  • 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 = :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);
}
  • 장점: JPAL을 그대로 사용하므로 객체 기준으로 쿼리를 작성하면서도 동시에 지연/즉시 로딩과 같은 고민 없이 원하는 모습으로 데이터를 조회할 수 있음

5.10 하이버네이트 @Subselect 사용

  • @Subselect : 쿼리 결과를 @Entity로 매핑할 수 있는 기능
  • 특징
    • 조회 쿼리를 값으로 가짐
    • @Subselect로 조회한 @Entity 수정 불가
    • @Immutbale : 엔티티의 매핑 필드/프로퍼티가 변경돼도 하이버네이트가 DB에 반영되지 않고 무시함.
    • @Synchronize : 엔티티와 관련한 테이블 목록을 명시함.
  • 장점: 일반 @Entity와 같으므로 EntityManager#find(), JPQL, Criteria를 사용하여 조회할 수 있음
  • 서브쿼리를 사용하고 싶지 않다면 네이티브 SQL 쿼리를 사용하거나 mybatis와 같은 별도 매퍼를 사용하여 조회 기능을 구현해야 함.

0개의 댓글