[DDD] 도메인 주도 개발 시작하기 - 5장

Y_Sevin·2023년 8월 5일
0

5.1 시작에 앞서

  • CQRS : 명령(command) 모델과 조회(Query) 모델을 분리하는 패턴

5.2 검색을 위한 스펙

  • Specification : 검색 조건을 다양하게 조합해야 할 때 사용
    애그리거트가 특정 조건을 충족하는지 검사할 때 사용하는 인터페이스
public interface Specification<T> {
	public boolean isSatisfiedBy(T agg);
}
  • 리포지터리나 DAO 는 검색 대상을 걸러내는 용도로 스펙을 사용한다.
  • 아래 예시에서는 isSatisfiedBy를 통해 객체가 조건에 충족한지 알아봄
public class OrdererSpec implements Specification<Order> {

  private String ordererId;

  public boolean isSatisfiedBy(Order agg) {
    return agg.getOrdererId().getMemberId().getId().equals(ordererId);
  }

}


public List<Order> findAll(Specification<Order> spec) {
	List<Order> allOrders = findAll();
	return allOrders.stream()
			.filter(order -> sepc.isSatisfiedBy(order))
			.toList()
}

5.3 스프링 데이터 JPA 를 이용한 스펙 구현

스펙 인터페이스에서 지네릭 타입 파라미터는 JPA의 엔티티타입을 의미, JPA 크리테리아 API에서 조건을 표현할 때 Predicate를 생성

package org.springframework.data.jpa.domain;

public interface Specification<T> extends Serializable {
	@Nullable
	Predicate toPredicate(Root<T> root, 
    				CriteriaQuery<?> query, 
    				CriteriaBuilder cb);
}

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,
    					CriteriaBuilder cb) {
        return cb.equal(root.get(OrderSummary_.ordererId), ordererId);
    }
}

굳이 JPA Criteria를 사용해야할까..?
너무 복잡해 보임,

5.4 리포지터리/DAO에서 스펙 사용하기

  • 스펙을 충족하는 엔티티검색하고 싶다면 findAll() 메서드 사용
public interface OrderSummaryDao extends Repository<OrderSummary, String> {
    List<OrderSummary> findAll(Specification<OrderSummary> spec);
}

// 스펙 객체 생성하고
Specification<OrderSummary> spec = new OrdererIdSpec("user1");
// findAll() 메서드를 이용해서 검색
List<OrderSummary> results = orderSummaryDao.findAll(spec);

5.5 스펙 조합

  • 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, 
    				CriteriaBuilder cb);
	}
}
// 조합 예시
  default Specification<T> and(@Nullable Specification<T> other) { ... }
  default Specification<T> or(@Nullable Specification<T> other) { ... }
static <T> Specification<T> not(@Nullable Specification<T> spec) { ... }
  static <T> Specification<T> where(@Nullable Specification<T> spec) { ... }

5.6 정렬 지정하기

스프링 데이터 JPA 는 두가지 방법으로 정렬을 지정할 수 있다.

  • 메서드 이름에 OrderBy를 사용해서 정렬 기준 지정
  • Sort를 인자로 전달

OrderBy

public interface OrderSummaryDao extends Repository<OrderSummary, String> {
    List<OrderSummary> findByOrdererIdOrderByNumberDesc(String ordererId);
  • 방법은 간단하지만 정렬 기준이 프로퍼티가 두개 이상이면 메서드 이름이 길어질 수 있음
  • 메서드 이름으로 순서가 정해지기 때문에 상황에 따라 정렬 순서를 변경할 수 없음

Sort

public interface OrderSummaryDao extends Repository<OrderSummary, String> {
    List<OrderSummary> findByOrdererId(String ordererId, Sort sort);
}
Sort sort1 = Sort.by("number").ascending();
Sort sort2 = Sort.by("orderDate").ascending();
Sort sort = Sort1.and(sort2);
List<OrderSummary> results = orderSummaryDao.findByOrdererId("user1",sort);

5.7 페이징 처리하기

  • 스프링 데이터 JPA는 페이징 처리를 위해 Pageable 타입을 이용 - - find메서드에 Pageable 타입 파라미터를 사용하면 페이징을 자동으로 처리함
public interface MemberDataDao extends Repository<MemberData, String> {
    
  List<MemberData> findByNameLike(String name, Pageable pageable);

}
PageRequest pageReq = PageRequest.of(1, 10);
List<MemberData> uesr = memberDataDao.findByNameLike("사용자%", pageReq);

5.8 스펙 조합을 위한 스펙 빌더 클래스

  • 스펙을 생성하다보면 스펙을 조합하는 경우가 생기는데 if와 스펙을 조합하는 코드가 같이 있다면 복잡성이 증가할 수 있음
  • 스펙 빌더를 사용한다면 코드 가독성과 구조를 단순화 시킬 수 있을
public class SpecBuilder {
    public static <T> Builder<T> builder(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> ifTure(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;
        }
    }
}

5.9 동적 인스턴스 생성

  • JPA 는 쿼리 결과에서 임의의 객체를 동적으로 생성할 수 있는 기능을 제공함
  • JPQL을 그대로 사용하기에 지연/즉시 로딩과 같은 고민이 필요없이 데이터를 조회할 수 있음

    JPQL : 쿼리 실행 전까지는 문법오류를 판단하지못함
    Criteria : 컴파일 단계에서 문법오류는 판단할 수 있음.. 하지만 가독성이 떨어지고 코드 자체의 복잡성이 증가하는 것 같음..
    QueryDSL : 내 눈에는 컴파일 단계에서 문법오류도 찾고 쿼리처럼 사용 할 수 있는 QueryDSL을 사용하는게 제일 좋아보인다....

5.10 하이버네이트 @Subselect 사용

  • @Subselect 는 쿼리 결과를 @Entity 로 매핑할 수 있는 기능
profile
매일은 아니더라도 꾸준히 올리자는 마음으로 시작하는 개발블로그😎

1개의 댓글

comment-user-thumbnail
2023년 8월 5일

정보에 감사드립니다.

답글 달기