Hyun's Wonderwall

[EFUB 4기 BE Lead] 도메인 주도 개발 시작하기 - 5. 스프링 데이터 JPA를 이용한 조회 기능 본문

Study/Java, Spring

[EFUB 4기 BE Lead] 도메인 주도 개발 시작하기 - 5. 스프링 데이터 JPA를 이용한 조회 기능

Hyun_! 2024. 3. 24. 21:00

EFUB 4기 BackEnd Lead_ 도메인 주도 개발 스터디

  • 스터디 커리큘럼: 최범균, "도메인 주도 개발 시작하기: DDD 핵심 개념 정리부터 구현까지"
  • 3주차 과제: Chapter 5. 스프링 데이터 JPA를 이용한 조회 기능, Chapter 6. 응용 서비스와 표현 영역

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

  • Keywords: 스펙, JPA 스펙 구현, 정렬과 페이징, 동적 인스턴스와 @Subselect

5.1 시작에 앞서

CQRS: 명령 모델과 조회 모델을 분리하는 패턴.

- 명령(Command) 모델: 상태를 변경하는 기능을 구현할 때 사용. // ex. 회원가입, 암호 변경, 주문 취소, 배송지 변경

  ㄴ 도메인 모델(엔티티, 애그리거트, 리포지터리 등)은 명령 모델로 주로 사용된다.

- 조회(Query) 모델: 데이터를 조회하는 기능을 구현할 때 사용한다. // ex. 주문 목록, 주문 상세

  ㄴ 정렬, 페이징, 검색 조건 지정 등이 조회 모델 구현으로 주로 사용된다.

5.2 검색을 위한 스펙

스펙(Specification): 다양한 검색 조건을 조합해야 할 때 사용한다. 목록 조회와 같은 기능에서 필요. (find 메서드를 필요한 조합마다 정의한다면 비효율적.)

- 스펙은 애그리거트가 특정 조건을 충족하는지를 검사할 때 사용하는 인터페이스다.

- 스펙을 리포지터리에 쓰면 agg는 애그리거트 루트가 되고, 스펙을 DAO에 쓰면 agg는 검색 결과로 리턴할 데이터 객체가 된다.

public interface Specification<T> {
	public boolean isSatisfiedBy(T agg); // agg: 검사 대상이 되는 객체
}

 

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

- ex. OrdererSpec: 특정 고객 id(ordererId)와 Order agg 객체의 주문자 정보가 일치하는지 확인하는 스펙을 구현

리포지터리나 DAO는 검색 대상을 걸러내는 용도로 스펙을 사용한다. 만약 리포지터리가 메모리에 모든 애그리거트를 보관하고 있다면 다음과 같이 스펙을 사용할 수 있다.

- ex. MemoryOrderRepository의 findAll: 모든 Order를 찾아 stream 필터로 isSatisfiedBy()로  걸러내는 코드

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

- ex. new 키워드로 객체 만들어서 넘겨줌. 하지만 실제 스펙을 이렇게 구현하지 않는다. (이유: 모든 애그리거트 객체를 메모리에 보관하기 어렵고, 조회 성능에 심각한 문제 발생.)

실제 스펙은 사용하는 기술에 맞춰 구현한다. 이 장에서는 스프링 데이터 JPA를 이용해 스펙을 구현한다.

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

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

// Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb);

스펙 인터페이스에서 제네릭 타입 파라미터 T는 JPA 엔티티 타입을 의미한다.

toPredicate() 메서드는 JPA 크리테리아(Criteria) API에서 조건을 표현하는 Predicate을 생성한다.

 

*다음에 해당하는 스펙 구현하기: (1) 엔티티 타입=OrderSummary, (2) ordererId 프로퍼티 값이 지정한 값과 동일.

(생략)
public class OrdererIdSpec implements Sepcification<OrderSummay> {
    private String ordererId;
    
    public OrdererIdSped(String ordererId) {
    	this.ordererId = ordererId;
    }
    @Override
    Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
    	return cb.equal(root.get(OrderSummay_.ordererId), ordererId);
    }
}

- OrdererIdSpec 클래스는 Specification<OrderSummary> 타입을 구현: OrderSummay에 대한 검색 조건을 표현함.

- toPredicate() 메서드 구현: ordererId 프로퍼티 값이 생성자로 전달받은 ordererId와 동일한지 비교하는 Predicate 생성.

 

JPA 정적 메타 모델
OrderSummay_ 클래스는 JPA 정적 메타 모델을 정의한 코드이다. (OrderSummay 클래스의 메타 모델)
- 정적 메타 모델은 @StaticMetamodel 애너테이션으로 관련 모델을 지정한다.
- 메타 모델 클래스는 모델 클래스의 이름 뒤에 '_'를 붙인 이름을 갖는다.
- 정적 메타 모델 클래스는 대상 모델의 각 프로퍼티와 동일한 이름을 갖는 정적 필드를 정의한다.
- 정적 메타 모델을 사용하는 대신 문자열로 프로퍼티를 지정할 수도 있으나, Criteria를 사용할 때는 정적 메타 모델 클래스를 사용하는 것이 좋다.
- 하이버네이터와 같은 JPA 프로바이더에서 정적 메타 모델을 생성하는 도구를 제공하므로 사용하면 편리하다.

 

스펙 구현 클래스를 개별적으로 만들지 않고 별도 클래스에 스펙 생성 기능을 모아도 된다. 예를 들어 OrderSummay와 관련된 스펙 생성 기능을 한 클래스에 모을 수 있다.

스펙 인터페이스는 함수형 인터페이스이므로 람다식을 이용해서 객체를 생성할 수 있다. 스펙 생성이 필요한 코드는 스펙 생성 기능을 제공하는 클래스를 이용해 조금 더 간결하게 스펙을 생성할 수 있다.

OrderSummarySpecs 클래스에서 public static Specification<OrderSummay> 자료형의 ordererId 메서드와 orderDateBetween 메서드를 정의함.
스펙 인터페이스가 함수형 인터페이스(1개의 추상 메서드를 갖는 인터페이스)이기 때문에 람다식으로( () -> ~ ) toPredicate 메서드를 구현해 객체 생성 가능.

 

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

and()와 or() 메서드는 기본 구현을 가진 디폴트 메서드임.

- and() : 두 스펙을 모두 충족하는 표현하는 스펙을 생성

- or() : 두 스펙 중 하나 이상 충족하는 조건을 표현하는 스펙을 생성.

다음 코드에서 spec1.and(spec2)는 spec1과 spec2를 모두 충족하는 조건을 표현하는 spec3을 생성한다.

- not 메서드도 제공. 조건을 반대로 적용할 때 사용함.

- null 가승성 있는 스펙 객체와 다른 스펙을 조합해야 할 때 NullPointerException 주의해야 하는데 - where() 메서드 사용하면 해결. where() 메서드는 스펙 인터페이스의 정적 메서드로 null을 전달하면 아무 조건도 생성하지 않는 스펙 객체를 리턴하고 null이 아니면 인자로 받은 스펙 객체를 그대로 리턴한다.

5.6 정렬 지정하기

스프링 데이터 JPA는 두 가지 방법을 사용해서 정렬을 지정할 수 있따.

1. 메서드 이름에 OrderBy를 사용해 정렬 기준 지정 (메서드 이름으로 정렬 순서 정해짐)

2. Sort를 인자로 전달

특정 프로퍼티로 조회하는 find 메서드는 이름 뒤에 OrderBy를 사용해 정렬 순서를 지정할 수 있따.

 

다음 조회 쿼리를 생성: ordererId 프로퍼티 값을 기준으로 검색 조건 지정, number 프로퍼티 값 역순으로 정렬

두 개 이상의 프로퍼티에 대한 정렬 순서를 지정할 수도 있다. 예를 들어 다음 메서드는 먼저 OrderDate 프로퍼티를 기준으로 내림차순으로 정렬하고 다음에 Number 프로퍼티를 기준으로 오름차순으로 정렬하는 쿼리를 생성한다.

: findByOrdererIdOrderByOrderDateDescNumberAsc

메서드 이름이 너무 길어지는 단점이 있다. 이럴 땐 Sort 타입을 사용!

스프링 데이터 JPA는 정렬 순서를 지정할 때 사용할 수 있는 Sort 타입을 제공한다.

다음은 정렬 순서를 지정하기 위해 Sort 타입을 파라미터로 갖는 메서드 예이다.

find 메서드에 마지막 파라미터로 Sort를 추가했다. 스프링 데이터 JPA는 파라미터로 전달받은 Sort를 이용해 알맞게 정렬 쿼리를 생성한다. find 메서드를 사용하는 코드는 알맞은 Sort 객체를 생성해서 전달하면 된다.

5.7 페이징 처리하기

스프링 데이터 JPA는 페이징 처리를 위해 Pageable 타입을 사용한다.

Sort 타입과 마찬가지로 find 메서드에 Pageable 타입 파라미터를 사용하면 페이징을 자동으로 처리해 준다.

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

스펙 빌더를 작성하면 코드 가독성 높아지고 구조 단순해진다.

5.9 동적 인스턴스 생성

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

조회 전용 모델을 만드는 이유: 표현 영역을 통해 사용자에게 데이터를 보여주기 위함

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

5.10 하이버네이트 @Subselect 사용

하이버네이트가 제공하는 JPA 확장 기능 @Subselect - 쿼리 결과를 @Entity에 매핑할 수 있는 기능