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

시작에 앞서

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

검색을 위한 스펙

검색 조건을 다양하게 조합해야 할 때 사용할 수 있는 것이 스펙이다. 스펙은 애그리거트가 특정 조건을 충족하는지를 검사할 때 사용하는 인터페이스다.

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

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

public interface Specification<T> extends Serializable {
    // not, where, and, or 메서드 생략

    @Nullable
    Predicate toPredicate(Root<T> root,
                    CriteriaQuery<?> query,
                    CriterialBuilder cb);
}
public class OrderIdSpec implements Specification<OrderSummary> {

    private String orderId;
    
    public OrdererIdSpec(String orderId) {
        this.ordererId = oderrerId;
    }
    
    @Override
    public Predicate toPredicate(Root<OrderSmmary> root,
                            CriterialQuery<?> query,
                            CriteriaBuilder cb) {
        return cb.equal(root.get(OrderSummary_.ordererId), ordererId);
    }
}

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

public class OrderSummarySpec {
    public static Specification<OrderSummary> orderId(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()메서드를 사용하면 된다.

public interface OrderSummaryDao extends Repository<OrderSummary, String> {
    List<OrderSummary> findAll(Specification<OrderSummary> spec);
}
// 스펙 객체를 생성하고
Specification<OrderSummary> spec  new OrdererIdSpec("user1");

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

스펙 조합

public interface Specification<T> extends Serializable {
    
    default Specification<T> and(@Nullable Specification<T> other) { ... }
    default Specification<T> or(@Nullable Specification<T> other) { ... }

    @Nullable
    Predicate toPredicate(Root<T> root,
                    CriteriaQuery<?> query,
                    CriterialBuilder cb);
}

정렬 지정하기

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

1 . 메서드 이름에 OrderBy를 사용해서 정렬 기준 지정

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

findByOrdererIdOrderByNumberDesc 메서드는 다음 조회 쿼리를 생성한다.

  • ordererId 프로퍼티 값을 기준으로 검색 조건 지정

  • number 프로퍼티 값 역순으로 정렬

2 . 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("name").ascending(); 
List<OrderSummary> results = orderSummaryDaofindByOrdererId("user1", sort)l;

아래와 같이 두 개 이상의 정렬 순서를 짧게도 표현 가능

Sort sort = Sort.by("number").ascending().and(Sort.by("orderDate").descending());

페이징 처리하기

public interface MemberDataDao extends Repository<MemberData, String> {
    List<MemeberData> findByNameLike(String name, Pageable pageable);
    Page<MemberData> findByBlocked(boolean blocked, Pageable pageable);
    Page<MemberData> findAll(Specification<MemberData> spec, Pageable pageable);
}
PageRequest pageReq = PageRequest.of(1, 10);
List<MemberData> user = memberDataDao.findByNameLike("사용자%", pageReq);

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

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));

동적 인스턴스 생성

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

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

하이버네이트 @Subselect 사용

하이버네이트는 JPA 확장 기능으로 @Subselect를 제공하는데, @Subselect는 쿼리 결과를 @Entity로 매핑할 수 있는 유용한 기능이다.

서브 쿼리를 사용하고 싶지 않다면 네이티브 SQL 쿼리를 사용하거나 마이바티스와 같은 별도 매퍼를 사용해서 조회 기능을 구현해야 한다.

Last updated