5.1 시작에 앞서
CQRS
- 명령(Command) 모델과 조회(Query) 모델을 분리하는 패턴.
- 명령 모델은 상태를 변경하는 기능을 구현할 때 사용.
- 조회 모델은 데이터를 조회하는 기능을 구현할 때 사용.
- 예를 들어 회원 가입, 암호 변경, 주문 취소 처럼 상태를 변경하는 기능을 구현할 때 사용.
- 도메인 모델은 명령 모델로 주로 사용된다.
5.2 검색을 위한 스펙
검색 조건이 고정되어 있지 않고, 다양한 검색 조건을 조합해야 할 때가 있다. 필요한 조합마다 find 메서드를 정의할 수 있지만, 이것은 좋은 방법이 아니다.
이렇게 검색 조건을 다양하게 조합해야 할 때 사용할 수 있는 것이 스펙이다. 스펙은 애그리거트가 특정 조건을 충족하는지를 검사할 때 사용하는 인터페이스다. 스펙 인터페이스는 다음과 같이 정의한다.
public interface Speficiation<T> {
public boolean isSatisfiedBY(T agg);
}
isSatisfiedBy() 메서드의 agg 파라미터는 검사 대상이 되는 객체다. 스펙을 리포지터리에 사용하면 agg는 애그리거트 루트가 되고, 스펙을 DAO에 사용하면 agg는 검색 결과로 리턴할 데이터 객체가 된다.
isSatisfiedBy() 메서드는 검사 대상 객체가 조건을 충족하면 true를 리턴하고, 그렇지 않으면 false를 리턴한다. 아래는 예시 코드이다.
public class OrdererSpec implements Speficiation<Order>{
private String orderId;
public OrdererSpec(String orderId){
this.orderId = orderId;
}
public boolean isSatisfiedBy(Order agg){
return agg.getOrdererId().getMemberId().getId().equals(orderId);
}
}
위의 코드는 Order 애그리거트 객체가 특정 고객의 주문인지 확인하는 스펙을 구현한 것이다.
Repository나 DAO는 위의 스펙을 사용하여 검색 대상을 걸러내는 용도로 사용한다. (흠... find 메소드를 생성하는 것 보다는 Spec을 사용해 검색을 하는 것 같은데... QueryDSL과 어떤 장단점이 있는 것인가...)
하지만 실제 스펙은 이렇게 구현하지 않는다.... (?)
5.3 스프링 데이터 JPA를 이용한 스펙 구현
스프링 데아ㅣ터 JPA는 검색 조건을 표현하기 위한 인터페이스인 Specification을 제공한다. (아오...)
따라서 다음 조건을 구현하는 스펙은 아래 코드와 같이 구현할 수 있다.
[조건]
- 엔티티 타입이 OrderSummary다.
- OrderId 프로터티 값이 지정한 값과 동일하다.
import java.util.function.Predicate;
public class OrdererIdSpec implements Specification<OrderSummary>{
private String ordererId;
public OrdererIdSpec(String ordererId){
this.ordererId = ordererId;
}
@Override
public Predicate toPredicate(Root<OrderSummary> root,
CriteriaQuery<?> query,
CriteriaBVuilder cb){
return cb.equal(root.get(OrderSummary_.ordererId), orderId);
}
}
해당 클래스를 개별적으로 만들지 않고 별도 클래스에 스펙 생성 기능을 모아도 된다. (유지 보수에 매우 용이할 듯 하다...)
5.8 스펙 조합을 위한 스펙 빌더 클래스
스펙을 생성하다 보면 다음 코드처럼 조건에 따라 스펙을 조합해야 할 때가 있다.
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> result = memberDataDao.findAll(spec, PageRequest.of(0, 5));
이 코드는 if와 각 스펙을 조합하는 코드가 섞여 있어 실수하기 좋고 복잡한 구조를 갖는다. 이 부분을 보완하기 위해 스펙 빌더를 사용하면 용이하다.
Specification<MemberData> spec = SpecBuilder.builder(MemberData.class)
.ifTrue(searchRequest.isOnlyNotBlocked(),
() -> MemberDataSpecs.nonBlocked())
.ifHasText(searchRequest.getName(),
name -> MemberDataSpecs.nameLike(sear chRequest.getName()))
.toSpec();
List<MemberData> result = memberDataDao.findAll(spec, PageRequest.of(0, 5));
이렇게 메소드로 진행되면 코드가 아니라 거의 영문 독해하는 기분이다... ㅋㅋㅋㅋ 이런 코드가 좋은 코드겠지요.
5.9 동적 인스턴스 생성
JPA는 쿼리 결과에서 임의의 객체를 동적으로 생성할 수 있는 기능을 제공하고 있다. (이건 원래 알고 있었다...)
JPQL의 select 절에는 new 키워드가 있는데 해당 키워드 뒤에 생성할 인스턴 스의 완전한 클래스 이름을 지정하고 괄호 안에 생성자에 인자로 전달할 값을 지정할 수 있다.(이건 몰랐음...)
'programming > DDD' 카테고리의 다른 글
[DDD] chapter 7 (0) | 2024.02.16 |
---|---|
[DDD] chapter 6 (0) | 2024.02.10 |
[DDD] chapter 4 (0) | 2024.02.03 |
[DDD] chapter 3 (0) | 2024.01.18 |
[DDD] chapter2 (1) | 2024.01.13 |