> For the complete documentation index, see [llms.txt](https://jihunparkme.gitbook.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://jihunparkme.gitbook.io/docs/book/ddd-start/05.md).

# 05.Spring Data JPA를 이용한 조회 기능

## 사작에 앞서

CQRS(Command and Query Responsibility Segregation)

* 명령 모델과 조회 모델을 분리하는 패턴
* `명령 모델`은 상태를 변경하는 기능을 구현할 때 사용(도메인 모델)
* `조회 모델`은 데이터를 조회하는 기능을 구현할 때 사용(조회 모델)

조회 모델을 구현할 때 **JPA, myBatis, jdbcTemplate 등 다양한 기술이 사용**된다.

* 모든 DB 연동 코드를 JPA만 사용해서 구현해야 한다고 생각하지 말자.

## 검색을 위한 스펙

`스펙`(Specification)

* 검색 조건을 다양하게 조합해야 할 때 사용할 수 있는 것
* 애그리거트가 특정 조건을 충족하는지를 검사할 때 사용하는 인터페이스

스펙 인터페이스 정의

```java
public interface Specification<T> {
    public boolean isSatisfiedBy(T agg);
}
```

스펙을 **리포지터리에 사용하면 agg(검사 대상 객체)는 애그리거트 루트**가 되고, 스펙을 **DAO에 사용하면 agg는 검색 결과**로 리턴할 데이터 객체가 된다.

isSatisfiedBy() 메서드는 검사 대상 객체가 조건을 충족하면 true, 그렇지 않으면 false 리턴

```java
public class OrdererSpec implements Specification<Order> {
	private String ordererId;
    ...

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

리포지터리나 DAO는 **검색 대상을 걸러내는 용도**로 스펙을 사용

```java
public class MemoryOrderRepository implements OrderRepository {
    ...
    public List<Order> findAll(Specification<Order> spec) {
        List<Order> allOrders = findAll();
        return allOrders.stream()
                        .filter(order -> spec.isSatisfiedBy(order))
                        .toList();
    }
}
```

리포지터리가 스펙을 이용해서 검색 대상을 걸러주므로 특정 조건을 충족하는 애그리거트를 찾고 싶으면 **원하는 스펙을 생성해서 리포지터리에 전달**해 주기만 하면 된다.

* 실제 스펙은 이렇게 구현하지 않고, Spring Data JPA를 이용

```java
Specification<Order> ordererSpec = new OrdererSpec("madvirus");
List<Order> orders = orderRepository.findAll(ordererSpec);
```

## Spring Data JPA를 이용한 스펙 구현

Spring Data JPA는 검색 조건을 표현하기 위한 인터페이스인 `Specification` 제공

* 제네릭 타입 파라미터 `T`는 **JPA 엔티티 타입**
* `toPredicate()` 메서드는 JPA Criteria API에서 조건을 표현하는 **Predicate 생성**

```java
package org.springframework.data.jpa.domain;

public interface Specification<T> extends Serializable {
    ...

    @Nullable
    Predicate toPredicate(Root<T> root, 
                          CriteriaQuery<?> query, 
                          CriteriaBuilder criteriaBuilder);
}
```

엔티티 타입이 **OrderSummary**이고, **orderId** 프로퍼티 값이 지정한 값과 동일한 스펙

* OrderSummary에 대한 검색 조건 표현
* toPredicate() 메서드는 ordererId 프로퍼티 값이 생성자로 받은 ordererId와 동일한지 비교하는 Predicate 생성
* `OrderSummary_`는 JPA 정적 메타 모델.
  * 하이버네이트와 같은 JPA 프로바이더는 정적 메타 모델을 생성하는 도구를 제공

```java
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);
	}
}
```

스펙 구현 클래스를 개별적으로 만들지 않고, **별도 클래스에 스펙 생성 기능을 모아**도 된다.

* 스펙 인터페이스는 함수형 인터페이스이므로 람다식 이용도 가능

```java
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> orderDateBetween(
            LocalDateTime from, LocalDateTime to) {
        return (Root<OrderSummary> root, CriteriaQuery<?> query, CriteriaBuilder cb) ->
                cb.between(root.get(OrderSummary_.orderDate), from, to);
    }
}

...

Specification<OrderSummary> betweenSpec = OrderSummarySpecs.orderDetailBetween(from, to);
```

## Repository/DAO에서 스펙 사용하기

스펙을 충족하는 엔티티를 검색하고 싶다면, `findAll()` 메서드를 사용

* 스펙 인터페이스를 파라미터로 갖음

```java
public interface OrderSummaryDao extends Repository<OrderSummary, String> {
    List<OrderSummary> findAll(Specification<OrderSummary> spec);
    List<OrderSummary> findAll(Specification<OrderSummary> spec, Sort sort);
    List<OrderSummary> findAll(Specification<OrderSummary> spec, Pageable pageable);
    ...
}

...

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

## 스펙 조합

Spring Data JPA 스펙 인터페이스는 스펙을 조합할 수 있는 두 메서드(`and`, `or`)를 제공

* 기본 구현을 가진 디폴트 메서드

```java
public interface Specification<T> extends Serializable {
    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);
    }
    ...
}

...

Specification<OrderSummary> spec = OrderSummarySpecs.ordererId("user1")
        .and(OrderSummarySpecs.orderDateBetween(from, to));
```

`not()` 메서드는 정적 메서드로 조건을 반대로 적용할 때 사용

```java
Specification<OrderSummary> spec = 
            Specification.not(OrderSummarySpecs.ordererId("user1"));
```

`where()` 메서드는 정적 메서드로 null을 전달하면 아무 조건도 생성하지 않는 스펙 객체를 리턴하고, null이 아니면 인자로 받은 스펙 객체를 그대로 리턴

```java
Specification<OrderSummary> spec = 
            Specification.where(createNullableSpec()).and(createOtherSpec());
```

## 정렬 지정하기

Spring Data JPA는 두 가지 방법을 사용해서 **정렬 지정** 가능

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

```java
List<OrderSummary> findByOrdererIdOrderByNumberDesc(String ordererId);
```

두 개 이상의 프로퍼티에 대한 정렬 순서를 지정할 경우

```java
List<OrderSummary> findByOrdererIdOrderByOrderDateDescNumberAsc(String ordererId);
```

메서드 이름에 OrderBy를 사용하는 방법은 간단하지만 정렬 기준의 프로퍼티가 두 개 이상이면 메서드 이름이 길어지는 단점이 존재하고, 상황에 따라 정렬 순서를 변경할 수도 없음.

이럴 경우, `Sort` 타입을 적용할 수 있다.

```java
List<OrderSummary> findByOrdererId(String ordererId, Sort sort);
List<OrderSummary> findAll(Specification<OrderSummary> spec, Sort sort);

...

Sort sort = Sort.by("number").ascending();
List<OrderSummary> results = orderSummaryDao.findByOrdererId("user1", sort);

Sort sort = Sort.by("number").ascending().and(Sort.by("orderDate").descending());
```

## 페이징 처리하기

Spring Data JPA는 **페이징 처리를 위해 Pageable 타입**을 이용

* find 메서드에 Pageable 타입 파라미터를 전달하면 페이징을 자동으로 처리
* Pageable 타입 객체는 PageRequest 클래스를 이용하여 생성
* 페이지 번호는 0번부터 시작
* 리턴 타입으로 Page 타입을 사용하면 데이터 목록과 함께 페이징 처리에 필요한 데이터도 함께 제공

```java
List<OrderSummary> findByOrdererId(String ordererId, Pageable pageable);
List<OrderSummary> findAll(Specification<OrderSummary> spec, Pageable pageable);
Page<OrderSummary> findAll(Pageable pageable);

...

PageRequest pageReq = PageRequest.of(1, 10); // 페이지 번호, 한 페이지의 개수 (11번째~20번째 데이터)
List<MemberData> user = memberDataDao.findByNameLike("사용자%", pageReq);
```

PageRequest, Sort 조합도 가능

```java
Sort sort = Sort.by("name").descending();
PageRequest pageReq = PageRequest.of(1, 2, sort);
List<MemberData> user = memberDataDao.findByNameLike("사용자%", pageReq);
```

> 프로퍼티를 비교하는 findBy프로퍼티 형식의 메서드는 Pageable 타입을 사용하더라도 리턴 타입이 List면 COUNT 쿼리를 실행하지 않는다.
>
> 페이징 처리와 관련된 정보가 필요 없다면 리턴 타입을 List로 사용해서 불필요한 COUNT 쿼리를 실행하지 않도록 하고, 페이징 처리가 필요할 때 리턴 타입을 Page로 사용하자.
>
> 반면, 스펙을 사용하는 findAll 메서드에 Pageable 타입을 사용하면 리턴 타입이 Page가 아니더라도 COUNT 쿼리를 실행한다. 이 경우 COUNT 쿼리를 실행하고 싶지 않다면 커스텀 리포지터리 기능을 이용해 보자.
>
> 참고. <https://javacan.tistory.com/entry/spring-data-jpa-range-query>

N개의 데이터만 조회할 경우

* First, Top 뒤에 숫자가 없을 경우 한 개의 결과만 리턴

```java
findFirst3ByNameLikeOrderByName(String name);
findFirstByBlockedOrderById(boolean blocked);
```

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

스펙 빌더를 사용하면 **조건에 따라 스펙을 조합**할 때 편리하다.

* if 블록을 사용하지 않으면서 코드 가독성을 높이고 구조를 단순하게 할 수 있다.

```java
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));
```

[스펙 빌더 클래스](https://github.com/jihunparkme/ddd-start2/blob/main/src/main/java/com/myshop/common/jpa/SpecBuilder.java)

## 동적 인스턴스 생성

JPA는 **쿼리 결과에서 임의의 객체를 동적으로 생성할 수 있는 기능**을 제공

* 조회 전용 모델을 만드는 이유는 표현 영역을 통해 사용자에게 데이터를 보여주기 위함
* 새로 추가한 밸류 타입을 알맞는 형식으로 출력하지 못하므로 값을 기본 타입으로 변환하면 편리

```java
public interface OrderSummaryDao extends Repository<OrderSummary, String> {
    @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);
}

...

public class OrderView {

    private final String number;
    private final OrderState state;
    private final String memberName;
    private final String memberId;
    private final String productName;

    public OrderView(OrderNo number, OrderState state, String memberName, MemberId memberId, String productName) {
        this.number = number.getNumber();
        this.state = state;
        this.memberName = memberName;
        this.memberId = memberId.getId();
        this.productName = productName;
    }
    ...
}
```

> 동적 인스턴스의 장점은 JPQL을 그대로 사용하므로 객체 기준으로 쿼리를 작성하면서도 동시에 지연/즉시 로딩과 같은 고민 없이 원하는 모습으로 데이터를 조회할 수 있다는 점이다.

## 하이버네이트 @Subselect 사용

하이버네이트는 JPA 확장 기능으로 `@Subselect`를 제공\`

* 쿼리 결과를 `@Entity`로 매핑할 수 있는 유용한 기능

```java
@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 {
    @Id
    private String number;
    private long version;
    @Column(name = "orderer_id")
    private String ordererId;
    @Column(name = "orderer_name")
    private String ordererName;
    ...
```

`@Immutable`, `@Subselect`, `@Synchronize`는 하이버네이트 전용 애노테이션인데, 이 태그로 테이블이 아닌 **쿼리 결과를 @Entity로 매핑** 가능하다.

`@Subselect`

* 조회 쿼리를 값으로 갖는다.
* select 쿼리 결과를 매핑할 테이블처럼 사용(DBMS의 뷰와 유사)
* 뷰와 동일하게 `@Subselect`로 조회한 `@Entity`는 수정 불가

`@Immutable`

* `@Subselect`를 이용한 `@Entity`의 매핑 필드 수정 시 하이버네이트가 변경 내역을 반영하여 **update 쿼리를 실행하는 문제를 방지**하기 위해 사용
* 매핑 테이블이 없으므로 에러가 발생하는데 이 문제를 방지
* `@Immutable`를 사용하면 해당 엔티티의 매핑 필드/프로퍼티가 변경되어도 DB에 반영하지 않고 무시

`@Synchronize`

* 엔티티 상태 변경 내역이 아직 테이블이 반영되지 않은 상태(커밋 전 시점)에서 조회 시 **최신 값이 아닌 이전 값이 담기는 문제를 해소**하기 위해 사용
* 해당 엔티티와 관련된 테이블 목록 명시
* 하이버네이트는 엔티티 로딩 전에 지정한 테이블과 관련된 변경이 발생하면 flush 수행

.

`@Subselect`를 사용해도 일반 `@Entity`와 같으므로 EntityManager#find(), JPQL, Criteria, Spec을 사용한 조회가 가능

```java
Specification<OrderSummary> spec = OrderSummarySpecs.orderDateBetween(from, to);
Pageable pageable = PageRequest.of(1, 1);
List<OrderSummary> results = orderSummaryDao.findAll(spec, pageable);
```

단, `@Subselect`는 지정 쿼리를 **from 절의 서브 쿼리로 사용**하는데, 서브 쿼리를 사용하고 싶지 않을 경우 **Natice SQL 쿼리를 사용하거나 myBatis 같은 별도 Mapper를 사용해서 조회 기능을 구현**해야 한다.
