-
05. 스프링 데이터 JPA를 이용한 조회 기능설계방법 2024. 5. 12. 13:31
5.1 시작에 앞서
시작에 앞서 언급할 것이 있다. 바로 CQRS다.
CQRS는 명령 모델과 조회 모델을 분리하는 패턴이다.
명령 모델은 상태를 변경하는 기능을 구현할 때 사용하고 조회 모델은 데이터를 조회하는 기능을 구현할 때 사용한다.
5.2 검색을 위한 스펙
검색 조건이 고정되어 있고 단순하면 다음과 같이 특정 조건으로 조회하는 기능을 만들면 된다.
public interface OrderDataDao{
Optional<OrderData> findById(OrderNo id);
List<OrderData> findByOrderer(String ordererId, Date fromDate, Date toDate);
}그런데 목록 조회와 같은 기능은 다양한 검색 조건을 조합해야 할 때가 있다.
필요한 조합마다 find메서드를 정의할 수 있지만 좋지않다. 정의할 find메서드가 계속 증가한다.
이렇게 검색 조건을 다양하게 조합해야 할 때 사용할 수 있는 것이 스펙이다.
스펙은 애그리거트가 특정 조건을 충족하는지를 검사할 때 사용하는 인터페이스다. 스펙 인터페이스는 다음과 같이 정의한다.
public interface Speficiation<T>{
public boolean isSatisfiedBy(T agg);
}isSatisfiedBy()메서드의 agg파라미터는 검사 대상이 되는 객체다.
스펙을 리포지터리에 사용하면 agg는 애그리거트 루트가 되고, 스펙을 DAO에 사용하면 agg는 검색 결과로 리턴할 데이터 객체가 된다.
isSatisfiedBy() 메서드는 검사 대상 객체가 조건을 충족하면 true를 리턴하고, 그렇지 않으면 false를 리턴한다.
예를 들어 Orderer 애그리거트 객체가 특정 고객의 주문인지 확인하는 스펙은 다음과 같이 구현할 수 있다.
public class OrdererSpec implements Specification<Order>{
private String ordererId;
public OrdererSpec(String ordererId){
this.ordererID = ordererId;
}
public boolean isSatisfiedBy(Orderer agg){
return agg.getOrdererID().getMember().getId().equals(ordererId);
}
}리포지터리나 DAO는 검색 대상을 걸러내는 용도로 스펙을 사용한다. 만약 리포지터리가 메모리에 모든 애그리거트를 보관하고 있다면 다음과 같이 스펙을 사용할 수 있다.
=>가능한가? 물론 불가능하다. 스프링 JPA를 이용한 스펙 구현에 대해 알아보자.
5.3 스프링 데이터 JPA를 이용한 스펙 구현
스프링 데이터 JPA는 검색 조건을 표현하기 위한 인터페이스인 Specification을 제공하며 다음과 같이 정의되어 있다.
public interface Specification<T> extends Serializable{
@Nullable
predicate toPrediacte(Root<T> root,
CriteriaQuery<?> query,
CrieriaBuilder cb);
}여기서 T는 JPA 엔티티 타입을 의미한다. toPredicate() 메서드는 JPA 크리테리아 API에서 조건을 표현하는 Predicate를 생성한다.
다음 스펙을 생성해보자.
- 엔티티 타입이 OrderSummary다.
- ordererId 프로퍼티 값이 지정한 값과 동일하다.public class OrdererIdSpec implements Specification<OrderSummary>{
private String orderId;
public OrdererIdSpec(String ordereID){
this.ordererId = ordererID;
}
@Override
public Predicate toPredicate(Root<OrderSummary> root,
CriteriaQuery<?> query,
CriteriaBuilder cb){
return db.equal(root.get(OrderSummary_.ordererId),ordererId);
}
}OrdererIdSpec클래스는 Specification<OrderSummary> 타입을 구현하므로 OrderSummary에 대한 검색 조건을 표현한다.
toPredicate메서드는 ordererId 프로퍼티 값이 생성자로 전달받은 ordererId와 동일한지 비교하는 Predicate를 생성한다.
스펙 구현 클래스를 개별적으로 만들지 않고 별도 클래스에 스펙 생성 기능을 모아도 된다.
예를 들어 OrderSummary와 관련된 스펙 생성 기능을 다음과 같이 한 클래스에 모을 수 있다.
public class OrderSummarySpecs{
public static Specification<OrderSummary> ordererId(String ordererId){
return (Root<OrdererSummary> root, CriteriaQuery<?> query,
CriteriaBuilder cb) -> cb.equal(root.<String>get("ordererId"),ordererId);
}
public static Specification<OrderSummary> orderDateBetween(LocalDateTime from, LocalDateTime to){
return (Root<OrdererSummary> root, CriteriaQuery query,
CriteriaBuilder cb) -> cb.between(root.get(OrderSummary_.orderDate),from,to);
}
}스펙 생성이 필요한 코드는 스펙 생성 기능을 제공하는 클래스를 이용해서 조금 더 간결하게 스펙을 생성할 수 있다.
Specification<OrderSummary> betweenSpec = OrderSummarySpecs.orderDateBetween(from,to); 5.4 리포지터리/DAO 에서 스펙 사용하기
스펙을 충족하는 엔티티를 검색하고 싶다면 findAll() 메서드를 사용하면 된다.
findAll() 메서드는 스펙 인터페이스를 파라미터로 갖는다.
public interface OrderSummaryDao extends Repository<OrderSummary, String>{
List<OrderSummary> findAll(Specification<OrderSUmmary> spec);
}OrderSummary에 검색조건을 표현한 스펙 인터페이스를 파라미터로 갖는다.
// 스펙 객체 생성하고
Specification<OrderSummary> spec = new OrdererIdSpec("user1");
// findAll()메서드를 이용해서 검색
List<OrderSummary> results = orderSummaryDao.findAll(spec);5.5 스펙조합
스프링 JPA가 제공하는 스펙 인터페이스는 스펙을 조합할 수 있는 두 메서드를 제공하고 있다.
이 두 메서드는 and or이다.
and()와 or()은 기본 구현을 가진 디폴트 메서드다.
5.6 정렬 지정하기
OrderBy를 사용한 지정
Sort를 인자로 전달
5.7 페이징 처리하기
Pageable 타입을 이용한다.
Pageable타입은 PageRequest를 이용해서 생성한다.
PageRequest pageReq = PageRequest.of(1,10)
Pageable을 사용하는 메서드의 리턴타입이 Page일 경우 스프링 데이터 JPA는 목록 조회 쿼리와 함꼐 COUNT 쿼리도 실행해서 조건에 해당하는 데이터 개수를 구한다.
PAGE는 전체 개수, 페이지 개수등 페이징 처리에 필요한 데이터도 함께 제공한다.
List<MemberData> contents = page.getContent(); // 조회 결과 목록
long totalElements = pages.getTotalElements(); // 조건에 해당하는 전체 개수
int totalPage = pages.getTotalPages(); // 전체 페이지 번호
int number = pages.getNumber(); //현재 페이지 번호
int numberOfElements = page.getNumberOfElements(); // 조회 결과 개수
int size = page.getSize(); //페이지 크기
5.8 스펙조합을 위한 스펙 빌더 클래스
스펙을 생성하다 보면 스펙을 조합해야할 때가 있다. 이럴때 필요한 것이 스펙 빌더다.
Specification<MemberData> spec = SpecBuilder.builder(MemberData.class).ifTrue(searchRequest.isOnlyNotBlocked(),()->MemberDataSpecs.nonBlocked()).ifHasText(searchRequest.getName(), name -> MebmerDataSpecs.nameLike(searchRequest.getName()).toSpec();
'설계방법' 카테고리의 다른 글
7.도메인 기능 (3) 2024.09.04 6.응용 서비스와 표현 영역 (0) 2024.09.02 이벤트 스토밍 (0) 2024.05.11 04.리포지터리와 모델 구현 (0) 2024.05.11 03. 애그리거트 (0) 2024.05.09