검색을 위한 스펙
검색 조건이 고정되어 있고, 단순하다면 특정 조건으로 조회하는 기능을 만들면 된다.
public interface OrderDataDao {
Optional<OrderData> findById(OrderNo id);
List<OrderData> findByOrderer(String ordererId, Date fromDate, Date toDate);
...
}
그런데 목록 조회와 같은 기능은 다양한 검색 조건을 조합해야할 때가 있다.
물론 필요한 조합마다 find 메서드를 만드는 것도 방법일 수는 있지만, 좋은 방법은 아니다.
(조합이 증가할수록 정의해야할 find 메서드도 함께 증가하기 때문)
이렇게 검색 조건을 다양하게 조합해야 할 때 사용할 수 있는 것이 스펙(Specification)이다.
스펙은 애그리거트가 특정 조건을 충족하는지를 검사할 때 사용하는 인터페이스이다.
public interface Specification {
public boolean isSatisfiedBy(T agg);
}
isSatisfiedBy 메서드의 agg 파라미터는 검사 대상이 되는 객체이다.
public class MemoryOrderRepository implements OrderRepository {
public List<Order> findAll(Specification<Order> spec) {
List<Order> allOrders = findAll();
return allOrders.stream().filter(order -> spec.isSatisfiedBy(order)).toList();
}
}
우선 이런 식으로 스펙을 활용해서 작성한 코드가 효율적일까?
결국엔 findAll로 다 가져와서 필터링을 하기 때문에 이건 좋은 코드는 아닌 것 같다.
스프링 데이터 JPA를 활용해 이를 개선해보자.
스프링 데이터 JPA를 이용한 스펙 구현
스프링 데이터 JPA는 검색 조건을 표현하기 위한 인터페이스인 Specification을 제공한다.
public interface Specification<T> extends Serializable {
// not, where, and, or 메서드 존재
@Nullable
Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb);
}
public class OrderSummarySpecs {
public static Specification<OrderSummary> ordererId(String ordererId) {
return (Root<OrderSummary> root, CriteriaQuery<?> query,
CriteriaBuilder cb) ->
cb.equal(root.<String>get("ordererId"), orderId);
}
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);
}
}
이제 해당 스펙을 활용해 엔티티를 검색하고 싶다면 findAll() 메서드를 사용하면 된다.
public interface OrderSummaryDao extends Repository<OrderSummary, String> {
List<OrderSummary> findAll(Specification<OrderSummary> spec);
}
Specification<OrderSummary> spec = new OrdererIdSpec("user1");
List<OrderSummary> results = orderSummaryDao.findAll(spec);
public interface Specification<T> extends Serializable {
// not, where, and, or 메서드 존재
@Nullable
Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb);
}
아까 Specification 코드에서 and, or 등의 메서드가 존재한다.
and()와 or() 메서드는 우리의 상식과 부합한 기본 구현을 가진 디폴트 메서드이다.
Specification<OrderSummary> spec1 = OrderSummarySpecs.orderId("user1");
Specification<OrderSummary> spec2 = OrderSummarySpecs.orderDateBetween(
LocalDateTime.of(2022,1,1,0,0,0),
LocalDateTime.of(2022,1,2,0,0,0));
Specification<OrderSummary> spec3 = spec1.and(spec2);
정렬 지정하기
스프링 데이터 JPA는 두 가지 방법을 사용해서 정렬을 지정할 수 있다.
- 메서드 이름에 OrderBy를 사용해서 정렬 기준 지정
- Sort를 인자로 전달
List<OrderSummary> findByOrdererIdOrderByNumberDesc(String ordererId);
메서드 이름에 OrderBy를 사용하는 방법은 간단하지만 정렬 기준 프로퍼티가 두 개 이상이라면 메서드 이름이 길어지는 단점이 있다.
또한 메서드 이름으로 정렬 순서가 정해지기 때문에 상황에 따라 정렬 순서를 변경할 수도 없다.
이럴 때 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("user1", sort);
페이징 처리하기
목록을 보여줄 때 전체 데이터 중 일부만 보여주는 페이징 처리는 기본이다.
스프링 데이터 JPA는 페이징 처리를 위해 Pageable 타입을 제공한다.
find메서드에 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("사용자이름%", pageReq);
pageRequest의 첫 번째 인자는 페이지 번호를, 두 번째 인자는 한 페이지의 갯수를 의미한다.
해당 코드의 경우는 두 번째 페이지의 (0번 부터 시작함) 10개씩을 조회하므로 11~20 데이터를 조회한다.
'책 정리 > 도메인 주도 개발 시작하기' 카테고리의 다른 글
Chapter 7. 도메인 서비스 (0) | 2023.12.12 |
---|---|
Chapter 6. 응용 서비스와 표현 영역 (1) | 2023.12.12 |
Chapter 4. 리포지터리와 모델 구현 (0) | 2023.12.05 |
Chapter 3. 애그리거트 (0) | 2023.12.01 |
Chapter 2. 아키텍처 개요 (0) | 2023.11.28 |