5장 스프링 데이터 JPA를 이용한 조회 기능

박준수·2024년 8월 22일
0

DDD 스터디

목록 보기
5/11
post-thumbnail
post-custom-banner

5.2 검색을 위한 스팩

public interface OrderDataDao {
	Optional<OrderData> findById(OrderNo id);
	List<OrderData> findByOrderer(String orderId, Date formDate, Date toDate);
	...
}
  • 검색 질의문을 나타내기 위해서는 필요한 조합마다 find 메서드를 정의해야한다.
  • 이때의 문제는 조합이 증가할수록 정의해야 할 find 메서드가 함께 증가하는 문제가 발생한다.
  • 이렇게 검색 조건을 다양하게 조합해야 할 때 사용할 수 있는 것이 스펙이다.
  • 스펙 : 애그리거트가 특정 조건을 충족하는지를 검사할 때 사용하는 인터페이스이다.
public interface Speficiation<T> {
	public boolean isSatisfiedBy(T agg);
}
  • 그러나 모든 애그리거트 객체를 메모리에 보관하기도 어렵고 설사 메모리에 다 보관할 수 있다 하더라도 조회 성능에 심각한 문제가 발생한다.

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

  • 스프링 데이터 JPA는 검색 조건을 표현하기 위한 인터페이스인 Specification을 제공한다.

  • 스펙 인터페이스에서 지네릭 타입 파라미터 T는 JPA 엔티티 타입을 의미함
  • toPredicate() 메서드는 JPA 크리테리아 API에서 조건을 표현하는 Predicate를 생성함

스펙 구현 클래스

public class OrdererIdSpec implements Specification<OrderSummary> {
	private String ordererId;

	@Override
	public Predicate toPredicate(
		Root<OrderSummary> root, 
		CriteriaQuery<?> query, 
		CriteriaBuilder cb
	) {
			return cb.equal(root.get(OrderSummary_.ordererId), ordererId);
		}
}

@StaticMetamodel(OrderSummary.class)
public class OrderSummary_ {
	public static volatile SingularAttribute<OrderSummary, String> number;
	public static volatile SingularAttribute<OrderSummary, Long> number;
	public static volatile SingularAttribute<OrderSummary, String> ordererId;
	public static volatile SingularAttribute<OrderSummary, String> ordererName;
	...
}
  • 정적 메탈 모델은 @StaticMetamodel 애너테이션을 이용해서 관련 모델을 지정한다.
  • 메타 모델 클래스는 모델 클래스의 이름 뒤에 ‘_’을 붙인 이름을 갖는다.
  • 정적 메탈 모델 클래스는 대상 모델의 각 프로퍼티와 동일한 이름을 갖는 정적 필드를 정의한다.
  • 정적 필드는 프로퍼티에 대한 메타 모델로서 프로퍼티 타입에 따라 SingularAttribute, ListAttribute 등의 타입을 사용해서 메타 모델을 정의한다.
  • 정적 메타 모델을 사용하는 대신 문자열로 프로퍼티를 지정할 수도 있지만 휴먼 에러가 발생할 수 있기에 정적 메타 모델 클래스를 사용하는 것이 코드 안정성이나 생산성 측면에서 유리하다.
    • cb.equal(root.get(”ordererId”), ordererId)

스펙 생성 기능을 별도 클래스에 모은 예

public class OrderSummarySpecs {
	public static Specification<OrderSummary> ordererId(String ordererId) {
		return (Root<OrderSummary> root, CriteriaQuey<?> query, 
			CriteriaBuilder cb) -> 
				cb.equal(root.get(OrderSummary_.orderId), orderId);
	}

	public static Specification<OrderSummary> orderDateBetween(LocalDateTime from, LocalDateTime to) {
		return (Root<OrderSummary> root, CriteriaQuey<?> query, 
			CriteriaBuilder cb) -> 
				cb.equal(root.get(OrderSummary_.orderDate), from, to);
	}
}

→ 같은 애그리거트라면 스펙 생성 기능을 별도 클래스에 한번에 모아두는 것이 파일 수도 줄이고 개발자가 관리하기 쉬울 것 같다는 생각이 든다.

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

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

5.5 스펙 조합

  • .and() : 두 스펙을 모두 충족하는 조건을 표현하는 스펙을 생성
  • .or() : 두 스펙 중 하나 이상 충족하는 조건을 표현하는 스펙을 생성함
  • .not() : 정적 메서드로 조건을 반대로 적용할 때 사용함
  • .where() : 조건에 충족하는지 확인할 때 사용함
    • 스펙 인터페이스의 정적 메서드로 null을 전달하면 아무 조건도 생성하지 않는 스펙 객체를 리턴하고 null이 아니면 인자로 받은 스펙 객체를 그대로 리턴한다.

5.6 정렬 지정하기

List<OrderSummary> findByOrdererIdOrderByNumberDesc(String ordererId);
// ordererId 프로퍼티 값을 기준으로 검색 조건 지정
// number 프로퍼티 값 역순으로 정렬
-> 메서드의 길이가 길어지면서 가독성 감소, 메서드 이름으로 정렬 순서 정해지기 때문에 상황에 따른 정렬 순서 변경 어려움

Sort sort = Sort.by("number").ascending();
List<OrderSummary> results = orderSummaryDao.findByOrdererId("user1", sort);
  • 파라미터로 전달받은 Sort를 사용해서 알맞게 정렬 쿼리를 생성하면 해결!

5.7 페이징 처리하기

  • Pageable 타입 파라미터를 사용하여 페이징 처리
List<MemberData> findByNameLike(String name, Pageable pagealbe);

PageRequest pageReq = PageRequest.of(1, 10);
List<MemberData> user = memberDataDao.findByNameLike("사용자%", pageReq);
  • 메서드 타입이 Page일 경우에는 Count 쿼리도 발생함
  • 스펙을 사용하는 findAll 메서드에 Pagealbe 타입을 사용하면 리턴 타입이 Page가 아니어도 COUNT 쿼리를 실행함
  • 스펙을 사용하고 페이징 처리를 하면서 COUNT 쿼리를 실행하고 싶지 않을 경우 커스텀 리포지터리를 직접 구현해야 함 → https://javacan.tistory.com/entry/spring-data-jpa-range-query

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

  • 스펙을 생성하다 보면 조건에 따라 스펙을 조합해야 할 때가 있음
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> 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;
        }
    }
}
// 조건문으로 인해 복잡해 보임, 실수하기도 좋음
  @Test
    void compositeSpec() {
        SearchRequest searchRequest = new SearchRequest();
        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));
        logger.info("result: {}", result.size());
    }

// 가독성 향상
    @Test
    void specBuilder() {
        SearchRequest searchRequest = new SearchRequest();
        searchRequest.setOnlyNotBlocked(true);
        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));
        logger.info("result: {}", result.size());
    }

5.9 동적 인스턴스 생성

  • 객체를 동적을 생성할 수도 있음
@Query("""    
            select new com.myshop.order.query.dto.OrderView(
                o.number, o.state, m.name, m.id, p.name
            )
            from Order o join o.orderLines ol, Member m, Product p
            where o.orderer.memberId.id = :ordererId
            and o.orderer.memberId.id = m.id
            and index(ol) = 0
            and ol.productId.id = p.id
            order by o.number.number desc
            """)
    List<OrderView> findOrderView(String ordererId);
  • new 키워드 뒤에 생성할 인스턴스의 완전한 클래스 이름을 지정하고 괄호 안에 생성자에 인자로 전달할 값을 지정함

→ 이 방식 대신 Querydsl을 사용하는 것이 더 가독성 있고 동적 객체를 생성하기 좋아보이기도 함

5.10 하이버네이트 @Subselect 사용

@Entity
@Immutable
@Subselect(
        """
        select o.order_number as number,
        o.version,
        o.orderer_id,
        o.orderer_name,
        o.total_amounts,
        o.receiver_name,
        o.state,
        o.order_date,
        p.product_id,
        p.name as product_name
        from purchase_order o inner join order_line ol
            on o.order_number = ol.order_number
            cross join product p
        where
        ol.line_idx = 0
        and ol.product_id = p.product_id"""
)
@Synchronize({"purchase_order", "order_line", "product"})
public class OrderSummary {
	...
}
  • @Immutable, @Subselect, @Synchronize를 사용하면 테이블이 아닌 쿼리 결과를 @Entity로 매핑할 수 있음
  • @Subselect로 조회한 @Entity는 수정할 수 없다. (뷰처럼 읽기 전용)
  • @Immutable을 사용하면 하이버네이트는 해당 엔티티의 매핑/프로퍼티가 변경되도 DB에 반영하지 않고 무시한다.
  • @Synchronize는 해당 엔티티와 관련된 테이블 목록을 명시한다. 하이버네이트는 엔티티를 로딩하기 전에 지정한 테이블과 관련된 변경이 발생하면 플러시를 먼저 한다.
profile
방구석개발자
post-custom-banner

0개의 댓글