일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- java
- 큐시즘
- 아이템 28
- GitHub Actions
- 객체지향 쿼리 언어
- 이펙티브자바
- 아이템30
- 최범균
- chapter5. 스프링 데이터 jpa를 이용한 조회 기능
- 일ㅊ
- criteriaquery
- 아이템 25
- JPA
- Spring Batch
- 아이템 27
- JPQL
- 기업프로젝트
- chapter4. 리포지터리와 모델 구현
- 아이템29
- chatgpt 연동
- Domain Driven Design
- 아이템 26
- 아이템 24
- 아이템31
- 도메인 주도 개발 시작하기
- 자바 ORM 표준 JPA 프로그래밍
- 아이템 23
- cicd
- ddd
- jdbc
- Today
- Total
코딩은 마라톤
[도메인 주도 개발 시작하기] Chapter5. 스프링 데이터 JPA를 이용한 조회 기능 본문
CQRS
명령(Command) 모델과 조회(Query) 모델을 분리하는 패턴
명령 모델
상태를 변경하는 기능을 구현할 때 사용한다.
ex) 회원가입, 암호 변경, 주문 취소 등 상태(데이터)를 변경하는 기능
조회 모델
데이터를 조회하는 기능을 구현할 때 사용한다.
ex) 주문 목록, 주문 상세처럼 데이터를 보여주는 기능
용도
엔티티, 애그리거트, 리포지터리 등의 모델은 상태를 변경할 때 주로 사용한다.
즉 도메인 모델은 명령 모델로 주로 사용된다.
정렬, 페이징, 검색 조건 지정과 같은 기능은 조회 기능에서 사용한다.
즉 조회 모델을 구현할 때 주로 사용한다.
스펙
검색을 위한 스펙
검색 조건이 고정되어 있고 단순하면 다음과 같이 특정 조건으로 조회하는 기능을 만든다.
public interface OrderDataDao {
Optional<OrderData> findById(OrderNo id);
List<OrderData> findByOrderer(String ordererId, Date fromDate, Date toDate);
..
}
그러나 위의 경우가 아닌 목록 조회와 같은 기능은 다양한 검색 조건을 조합해야할 때가 있다.
필요한 조합마다 find 메서드를 정의할 수 있지만 이를 모두 구현하는 것은 좋은 방법이 아니다.
이때 검색 조건을 다양하게 조합해야 할 때 사용할 수 있는 것이 "스펙"이다.
스펙 : 애그리거트가 특정 조건을 충족하는지를 검사할 때 사용하는 인터페이스
public interface Speficiation<T> {
public boolean isSatisfiedBy(T agg);
}
isSatisfiedBy() 메서드는 검사 대상 객체(agg)가 조건을 충족하면 true, 그렇지 않으면 false를 리턴한다.
스펙을 리포지터리에 사용하면 agg는 애그리거트 루트가 되고, DAO에 사용하면 agg는 검색 결과로 리턴할 데이터 객체가 된다.
public class OrdererSpec implements Specification<Order> {
private String ordererId;
public OrdererSpec(String ordererId) {
this.ordererId = ordererId;
}
public boolean isSatisfiedBy(Order agg) {
return agg.getOrdererId().getMemberId().getId().equals(ordererId);
}
}
스펙은 리포지터리나 DAO에서 검색 대상을 걸러내는 용도로 스펙을 사용한다.
스프링 데이터 JPA를 이용한 스펙 구현
스프링 데이터 JPA는 검색 조건을 표현하기 위해 인터페이스인 Specifiaction 스펙 인터페이스를 제공한다.
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, @Nullable CriteriaQuery<?> query, CriteriaBuilder criteriaBuilder);
static <T> Specification<T> allOf(Iterable<Specification<T>> specifications) {
return (Specification)StreamSupport.stream(specifications.spliterator(), false).reduce(where((Specification)null), Specification::and);
}
@SafeVarargs
static <T> Specification<T> allOf(Specification<T>... specifications) {
return allOf((Iterable)Arrays.asList(specifications));
}
static <T> Specification<T> anyOf(Iterable<Specification<T>> specifications) {
return (Specification)StreamSupport.stream(specifications.spliterator(), false).reduce(where((Specification)null), Specification::or);
}
@SafeVarargs
static <T> Specification<T> anyOf(Specification<T>... specifications) {
return anyOf((Iterable)Arrays.asList(specifications));
}
}
제네릭 타입 T는 JPA 엔티티 타입을 의미한다.
toPredicate() 메서드는 JPA 크리테리아 API에서 조건을 표현하는 Predicate를 생성한다.
리포지터리/DAO에서 스펙 사용하기
스펙을 충족하는 엔티티를 검색하고 싶으면 findAll() 메서드를 사용한다.
public interface OrderSummaryDao extends Repository<OrderSummary, String> {
List<OrderSummary> findAll(Specification<OrderSummary> spec);
}
스펙 조합
위 스펙 인터페이스에서 제공하는 and(), or(), not(), where() 메서드가 존재한다.
이들을 조합해서 스펙을 조합할 수 있다.
참고) Elastic Search에서 Criteria Query를 사용한 적이 있는데 이때의 메서드와 동일한 기능을 하는 것으로 보인다.
정렬
스프링 데이터 JPA는 두 가지 방법을 사용해서 정렬을 지정한다.
- 메서드 이름에 OrderBy를 사용
- findByOrderIdOrderByNumberDescNumberAsc 이와 같이 사용
- 정렬 조건이 많아질 수록 메서드명이 길어지는 문제 발생 → 이때 Sort 인자 사용
- Sort 인자를 전달
public interface OrderSummaryDao extends Repository<OrderSummary, String> {
List<OrderSummary> findByOrdererId(String ordererId, Sort sort);
}
마지막 파라미터로 Sort를 추가한다.
Sort 객체를 생성해서 전달하면 된다.
Sort sort = Sort.by("number").ascending();
List<OrderSummary> results = orderSummaryDao.findByOrdererId("user1", sort);
페이징 처리
목록을 보여줄 때 전체 데이터 중 일부만 보여주는 페이징 처리는 Pageable 타입을 이용한다.
public interface MemberDataDao extends Repository<MemberData, String> {
List<MemberData> findByNameLike(String name, Pageable pageable);
}
Pageable 타입은 PageRequest 클래스를 이용해서 생성한다.
PageRequest pageReq = PageRequest.of(1,10);
// 첫 번째 인자 : 페이지 번호, 두 번째 인자 : 한 페이지의 개수 → 11번째 ~ 20번째 데이터 조회
정렬할 때 사용하는 Sort 인자 또한 PageRequest에서 세 번째 인자로 사용해서 정렬할 수 있다.
추가로 List가 아닌 Page 타입을 사용해서 반환하면 데이터 목록뿐만 아니라 조건에 해당하는 전체 개수도 구할 수 있다. (COUNT 쿼리 실행)
스펙 빌더 클래스
스펙을 생성하다 보면 여러 조건에 따라 스펙을 조합할 수도 있다.
이때 복잡한 구조를 갖는 스펙을 조합하는 코드일 경우 스펙 빌더를 만들어 사용할 수 있다.
Specification<MemberData> spec = SpecBuilder.builder(MemberData.class)
.ifTrue( /* 조건절 */,
() -> /* 실행문 */ )
.ifHasText( /* 조건절 */,
() -> /* 실행문 */ )
.toSpec();
// 빌더에 ifTrue(), ifHasText(), and() 메서드가 존재한다.
'Backend' 카테고리의 다른 글
[도메인 주도 개발 시작하기] Chapter4. 리포지터리와 모델 구현 (2) | 2024.10.13 |
---|---|
설명에 따른 책임을 이겨낼 것인가? (3) | 2024.10.06 |
[도메인 주도 개발 시작하기] Ch3. 애그리거트 (0) | 2024.09.22 |
[도메인 주도 개발 시작하기] Ch2. 아키텍처 개요 (1) | 2024.09.10 |
[도메인 주도 개발 시작하기] 책 선정 이유와 Ch1. 도메인 모델 시작하기 (0) | 2024.09.10 |