728x90
반응형
목차
CQRS 란
명령(Command)모델과 조회(Query) 모델을 분리하는 패턴.
명령 모델은 상태 변경 기능 구현시 사용, 조회 모델은 데이터 조회기능 구현시 사용.
ex) 명령모델 : 회원가입, 암호 변경, 주문 취소 > 상태를 변경
조회모델 : 주문 목록, 주문상세 > 데이터 보여주는 기능
도메인 모델은 명령모델로 주로 사용됨.
검색을 이용한 스펙
- 검색 조건이 고정되어 있고 단순하면 조회 기능을 만들면 된다.
public interface OrderDataDao{
Optional<OrderData> findById(OrderNo id);
List<OrderData> findByOrderer(String ordererId, Date fromDate, Date toDate);
...
}
- 다양한 검색조건을 조합해야할 때가 있음
- 필요한 조합마다 find메서드를 정의하는 것은 좋은 방법이 아님
- 다양하게 검색 조건을 조합할 때 사용하는 것이 Specification
- Specification : 애그리거트가 특정 조건을 충족하는지를 검사할 때 사용하는 인터페이스
public interface Specification<T> {
public boolean isSatisfiedBy(T agg);
}
- isSatisfiedBy() 메서드는 검사 대상 객체가 조건 충족시 true, 아니면 false 리턴
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);
}
}
- Order애그리거트 객체가 특정 고객의 주문인지 확인하는 스펙
public class MemberOrderRepository implements OrderRepository {
public List<Order> findAll(Specification<Order> spec) {
List<Order> allOrders = findAll();
return allOrders.stream().filter(order -> spec.isSatisfiedBy(order)).toList();
}
}
- 리포지터리나 DAO는 검색 대상을 걸러내는 용도로 사용
- 리포지터리가 모든 애그리거트 보관 시 위와 같이 사용 가능
//검색 조건을 표현하는 스펙을 생성
Specification<Order> ordererSpec = new OrdererSpec("madvirus");
//리포지터리에 전달
List<Order> orders = orderRepository.findAll(ordererSpec);
- 특정 조건을 충족하는 애그리거트를 찾고싶을 때 사용
- 실제 스펙은 위처럼 사용하지 않음
- 객체를 메모리에 보관하기 어렵고, 조회성능에 심각한 문제가 발생할 수 있음
스프링 데이터 JPA를 이용한 스펙 구현
- 스프링 데이터 JPA에서 제공하는 인터페이스 (Specification)
public interface Specification<T> extends Serializable {
@Nullable
Predicate toPredicate(Root<T> root,
CriteriaQuery<?> query,
CriteriaBuilder criteriaBuilder);
}
- 제네릭 타입 파라미터 T는 JPA 엔티티 타입을 의미함
- toPredicate() 는 JPA 크리테리아 API에서 조건을 표현하는 Predicate를 생성함
- Entity Type이 OrderSummary
- ordererId프로퍼티 값이 생성자로 전달받은 ordererId와 동일한지 비교하는 Predicate를 생성
- 24라인은 JPA 정적 메타 모델 (OrderSummary_.ordererId)
public class OrderSummarySpecs {
public static Specification<OrderSummary> ordererId(String ordererId) {
return (Root<OrderSummary> root,CriteriaQuery<?> query,
CriteriaBuilder cb) -> cb.equal(root.<String>get("ordererId"), ordererId);
}
public static Specification<OrderSummary> orderDataBetween(
LocalDateTime from, LocalDateTime to) {
return (Root<OrderSummary> root, CriteriaQuery<?> query,
CritefiaBuilder 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);
}
- OrderSummary에 대한 검색 조건을 표현하는 스펙 인터페이스를 파라미터로 가짐
// 스펙 객체 생성
Specification<OrderSummary> spec = new OrdererIdSpec("user1");
// findAll() 메서드를 이용해서 검색
List<OrderSummary> results = orderSummaryDao.findAll(spec);
스펙조합
- Spring Data JPA는 스펙을 조합할 수 있는 두 메서드 and, or를 제공
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,
CreteriaBuilder criteriaBuilder);
}
- and() : 두 스펙을 모두 충족하는 조건
- or() : 두 스펙 중 하나 이상 충족하는 조건
Specification<OrderSummary> spec1 = OrderSummarySpecs.ordererId("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);
- spec1.and(spec2) 는 spec1과 spec2를 모두 충족하는 spec3을 생성
Specification<OrderSummary> spec = OrderSummarySpecs.ordererId("user1")
.and(OrderSummarySpecs.orderDateBetween(from, to));
- 스펙마다 변수 선언하지 않고 바로 and() 사용가능
Specification<OrderSummary> spec = Specification.not(OrderSummarySpecs.ordererId("user1"));
Specification<OrderSummary> nullableSpec = createNullableSpec(); // null일 수 있음
Specification<OrderSummary> otherSpec = createOtherSpec();
Specification<OrderSummary> spec = nullableSpec == null?otherSpec:nullableSpec.and(otherSpec);
- not()은 조건을 반대로 적용할 때 사용
- null여부를 검사하는 방법은 귀찮음
Specification<OrderSummary> spec = Specification.where(createNullableSpec()).and(createOtherSpec());
- where()메서드는 null을 전달하면 아무조건도 생성하지 않는 스펙 개체 리턴 null이 아니면 인자로 받은 스펙 객체를 리턴
정렬 지정하기
- 메서드 이름에 OrderBy 사용해서 정렬기준 지정
- Sort 인자로 전달
public interface OrderSummaryDao extends Repository<OrderSummary, String> {
List<OrderSummary> findByOrdererIdOrderByNumberDesc(String ordererId);
}
- ordererId 프로퍼티값을 기준으로 검색 조건 지정
- number 프로퍼티 값 역순으로 정렬
findByOrdererIdOrderByOrderDateDescNumberAsc
- 두 개 이상 프로퍼티에 대한 정렬순서 지정시 위와 같이 사용
- 이름이 길어지는 단점이 있음
List<OrderSummary> findByOrdererId(String ordererId, Sort sort);
List<OrderSummary> findAll(Specification<OrderSummary> spec, Sort sort);
- Sort타입을 파라미터로 사용하면 정렬순서를 지정할 수 있다.
Sort sort = Sort.by("number").ascending();
List<OrderSummary> results = orderSummaryDao.findByOrdererId("user1", sort);
- "number"프로퍼티 기준 오름차순 정렬을 표현하는 sort 객체를 생성함
Sort sort1 = Sort.by("number").ascending();
Sort sort2 = Sort.by("orderDate").descending();
Sort sort = sort1.and(sort2);
- 두 개 이상의 정렬순서 지정하고 싶으면 Sort#and() 메서드 사용
Sort sort = Sort.by("number").ascending().and(Sort.by("orderDate").descending());
페이징 처리하기
- 스프링 데이터 JPA는 Pageable타입을 제공함
public interface MemberDataDao extends Repository<MemberData, String> {
List<MemberData> findByNameLike(String name, Pageable pageable);
}
PageRequest pageReq = PageRequest.of(1, 10);
List<MemberData> user = memberDataDao.findByNameList("사용자%", pageReq);
- PageRequest 첫 번째 인자는 페이지 번호, 두 번째 인자는 페이지 개수
- 페이지 번호는 0부터 시작하므로 1은 두 번째 페이지 조회(11~20번째까지)
Sort sort = Sort.by("name").descending();
PageRequest pageReq = PageRequest.of(1, 2, sort);
List<MemberData> user = memberDataDao.findByNameLike("사용자%", pageReq);
- Sort를 사용하면 정렬도 지정할 수 있음
- 리턴타입이 Page일 경우 JPA는 목록 쿼리와 함께 Count 쿼리도 실행해서 조건에 해당하는 데이터 개수를 구함
- 전체개수, 페이지 개수 등 페이지에 필요한 데이터도 함께 제공함
스펙 조합을 위한 스펙 빌더 클래스
- 스펙 생성시 아래와 같이 스펙을 조합해야하는 경우
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));
- 메서드 호출 체인으로 변수 할당을 줄임
public class SpecBuilder {
public static <T> Builder<T> build(Class<T> type) {
return new Builder<T>();
}
public static class Builder<T> {
private List<Specification<T>> specs = new ArrayList<>();
public Builder<T> and(Specification<T> spec) {
specs.add(spec);
return this;
}
public Builder<T> ifHasText(String str,
Function<String, Specification<T>> specSupplier) {
if(StringUtils.hasText(str)) {
specs.add(specSupplier.apply(str));
}
return this;
}
public Builder<T> ifTrue(Boolean cond,
Supplier<Specification<T>> specSupplier) {
if(cond != null && cond.booleanValue()) {
specs.add(specSupplier.get());
}
return this;
}
public Specification<T> toSpec() {
Specification<T> spec = Specification.where(null);
for(Specification<T> s : specs) {
spec = spec.and(s);
}
return spec;
}
}
}
동적 인스턴스 생성
- JPA는 쿼리 결과에서 임의의 객체를 동적으로 생성할 수 있는 기능을 제공함
- select절의 new 키워드 뒤에 생성할 인스턴스의 클래스이름 지정하고 괄호 안에 생성자에 인자로 전달할 값을 지정
- 조회 전용 모델을 만듬 > 표현영역을 통해 사용자에게 데이터를 보여주기 위해
하이버네이트 @Subselect 사용
- 하이버네이트는 JPA 확장으로 @Subselect를 제공함
- @Immutable, @Subselect, @Synchronize는 하이버네이트 전용 애너테이션인데 이 태그를 이용해 @Entity로 매핑할 수 있음
- @Subselect는 조회쿼리를 값으로 가짐 > @Entity는 수정 불가능
- @Subselect를 이용한 @Entity 매핑필드 수정시 변경내역을 반영하는 update 쿼리를 실행함 > 에러 발생(테이블이 없으므로)
- @Immutable > 엔티티의 매핑필드/프로퍼티 변경되도 DB반영하지 않고 무시함
- 하이버네이트는 트랜잭션 커밋 시점에 변경 사항을 DB에 반영하므로 data변경과 조회를 한 트랜잭션에 할 경우 변경내용을 조회하지 못한다 > 이 문제 해소 하기 위해 @Synchronize 사용 (변경내역이 있으면 플러시 먼저함)
728x90
반응형
'책리뷰 > 도메인 주도 개발 시작하기(DDD핵심 개념 정리부터 구현까지)' 카테고리의 다른 글
도메인 주도 개발(DDD) 도메인 서비스 (0) | 2022.05.24 |
---|---|
도메인 주도 개발 (DDD) 시작하기 응용서비스와 표현영역 (0) | 2022.05.19 |
도메인 주도 개발(DDD) 리포지터리와 모델 구현 (0) | 2022.05.13 |
도메인 주도 설계(DDD) 애그리거트 (0) | 2022.05.11 |
도메인 주도 설계 아키텍처 (0) | 2022.05.10 |