
대부분의 데이터 접근 계층(Data Access Layer)은 CRUD 로직에서 코드 중복이 필연적으로 발생한다. 이는 JPA를 사용한 개발에서도 동일하다.
예제 12.1. JPA의 반복적인 CRUD
public class MemberRepository {
@PersistenceContext
EntityManager em;
public void save(Member member) { ... }
public Member findOne(Long id) { ... }
public List<Member> findAll() { ... }
public Member findByUsername(String username) { ... }
}
public class ItemRepository {
@PersistenceContext
EntityManager em;
public void save(Item item) { ... }
public Member findOne(Long id) { ... }
public List<Member> findAll() { ... }
}
위와 같은 코드의 중복은 제네릭과 상속을 활용한 부모 클래스 정의로 해결할 수 있고, 이를 GenericDAO라 칭한다. 하지만 이 방법은 공통 기능을 구현한 부모 클래스에 종속되고 구현 클래스 상속이 가지는 문제점에 노출된다는 단점이 있다.
스프링 데이터 JPA는 스프링 프레임워크 프로젝트 중 JPA를 편리하게 사용할 수 있도록 지원하는 프로젝트이다. CRUD를 처리하기 위한 공통 인터페이스를 제공하여 리포지토리 로직 작성 시 인터페이스만 작성하면 스프링 데이터 JPA가 실행 시점에 구현 객체를 동적으로 생성하여 주입해 준다. 따라서 데이터 접근 계층 개발 시 인터페이스 작성만으로 개발을 완료할 수 있다.
CRUD를 처리하기 위한 공통 메서드는 스프링 데이터 JPA가 제공하는 org.springframework.data.jpa.repository.JpaRepository 인터페이스에 정의되어 있다.
예제 12.2. 스프링 데이터 JPA 적용
public interface MemberRepository extends JpaRepository<Member, Long> {
Member findByUsername(String username);
}
public interface ItemRepository extends JpaRepository<Item, Long> {
}

클래스 다이어그램은 위와 같다.
일반적인 CRUD 메서드는 JpaRepository 인터페이스가 공통으로 제공하며, MemberRepository.findByUsername()과 같이 별도로 정의한 메서드는 스프링 데이터 JPA가 메서드의 이름을 분석하여 다음과 같은 JPQL를 생성 및 실행한다.
생성된 JPQL
SELECT m FROM Member m WHERE username = :username

스프링 데이터(Spring Data) 프로젝트는 JPA, MongoDB, NEO4J, REDIS, HADOOP, GEMFIRE 같은 다양한 데이터 저장소에 대한 접근을 추상화하여 개발자에게 편의를 제공하고 데이터 접근 계층 코드의 반복을 줄여준다.
스프링 데이터 JPA는 이러한 스프링 데이터 프로젝트의 하위 프로젝트 중 하나로, JPA에 특화된 기능을 제공한다.
스프링 부트 환경을 기준으로 스프링 데이터 JPA는 프로젝트 설정 시 의존성 스타터를 지정할 수 있다.
예제 12.3. 스프링 데이터 JPA 메이븐 의존성 설정
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
레거시 스프링 프로젝트에서는 XML을 기반으로 <jpa:repositories> 태그에 리포지토리를 검색할 base-package 속성을 정의하곤 했다.
예제 12.4. XML 설정
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<tx:annotation-driven/>
<context:component-scan base-package="jpabook.jpashop.service, jpabook.jpashop.repository"/>
...
</beans>
최근에는 JavaConfig 설정으로 애플리케이션 코드를 통한 구성이 선호된다. 다음과 같이 org.springframework.data.jpa.repository.config.EnableJpaRepositories 애너테이션을 추가하고 basePackages 속성에 경로를 명시하면 된다.
예제 12.5. JavaConfig 설정
@Configuration
@EnableJpaRepositories(basePackages = "jpabook.jpashop.repository")
public class AppConfig {}
스프링 부트는 구성보다 관례 원칙에 따라 spring-boot-starter-data-jpa 의존성 추가 시 @SpringBootApplication 애너테이션으로 지정된 코드가 위치한 패키지 하위의 리포지토리들을 별도의 설정 없이 스캔한다.

이렇게 환경설정을 마치면 스프링 데이터 JPA는 애플리케이션 실행 시점에 사용자가 정의한 리포지토리 인터페이스의 계약을 이행하는 프록시 객체를 생성하여 영속성 컨텍스트의 스프링 빈으로 등록한다.
스프링 데이터 JPA가 제공하는 JpaRepository 인터페이스는 간단한 CRUD 기능을 공통으로 처리하기 위한 계약을 정의한다. 가장 단순한 사용법은 인터페이스 상속 후 제네릭에 엔터티 클래스 타입과 식별자 타입을 지정하는 것이다.
예제 12.6. JpaRepository 공통 기능 인터페이스
public interface JpaRepository<T, ID extends Serializable> extends PagingAndSortingRepository<T, ID> {
...
}
예제 12.7. JpaRepository를 사용하는 인터페이스
public interface MemberRepository extends JpaRepository<Member, Long> {}

위 그림은 JpaRepository 인터페이스의 계층 구조를 나타낸다. 가장 기본적으로는 스프링 데이터 모듈이 있고 그 안에 스프링 데이터 프로젝트가 공통으로 사용하는 Repository, CrudRepository, PagingAndSortingRepository 인터페이스가 정의되어 있다. 스프링 데이터 JPA 모듈의 JpaRepository 인터페이스는 이들을 상속하여 다음과 같이 JPA에 특화된 추가적인 기능을 제공한다.
save(S): 새로운 엔터티는 저장하고 이미 있는 엔터티는 수정한다.delete(T): 엔터티 하나를 삭제한다. 내부적으로 EntityManager.remove() 메서드를 호출한다.findOne(ID): 엔터티 하나를 조회한다. 내부적으로 EntityManager.find() 메서드를 호출한다.getOne(ID): 엔터티를 프록시로 조회한다. 내부적으로 EntityManager.getReference() 메서드를 호출한다.findAll(...): 모든 엔터티를 조회한다. 정렬(Sort)이나 페이징(Pageable) 조건을 인자로 제공할 수 있다.스프링 데이터 JPA는 메서드의 이름만으로 적절한 JPQL 쿼리를 생성하는 쿼리 메서드 기능을 지원한다. 쿼리 메서드 기능은 크게 3가지로 구분된다.
메서드 정의
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByAgeAndUsername(Integer age, String username);
}
생성된 쿼리
Hibernate:
/* <criteria> */
select
m1_0.id,
m1_0.age,
m1_0.username
from
member m1_0
where
m1_0.age=?
and m1_0.username=?
메서드 이름은 다음과 같은 규칙으로 작성한다. 만약 엔터티의 필드 이름이 변경되면 메서드 이름도 변경해야 한다. 그렇지 않으면 애플리케이션 시작 시점에 오류가 발생한다.

스프링 데이터 JPA는 메서드 이름으로 JPA 명명된 쿼리를 호출하는 기능을 제공한다. 같은 방법으로 네이티브 명명된 쿼리를 호출할 수도 있다.
예제 12.8. @NamedQuery 애너테이션으로 명명된 쿼리 정의
@Entity
@NamedQuery(
name = "Member.findByUsername",
query = "SELECT m FROM Member m WHERE m.username = :username"
)
public class Member {
...
}
예제 12.9. orm.xml의 XML 사용
<named-query name="Member.findByUsername">
<query>
<CDATA[
SELECT m
FROM Member m
WHERE m.username = :username
]>
</query>
</named-query>
JPA를 직접 사용하면 다음과 같이 명명된 쿼리를 호출한다.
예제 12.10. JPA를 직접 사용해서 명명된 쿼리 호출
public class MemberRepository {
public List<Member> findByUsername(String username) {
...
List<Member> resultList =
em.createNamedQuery("Member.findByUsername", Member.class)
.setParameter("username", "Yushin")
.getResultList();
}
}
스프링 데이터 JPA를 사용하면 다음과 같이 코드를 간소화할 수 있다.
예제 12.11. 스프링 데이터 JPA로 명명된 쿼리 호출
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsername(@Param("username") String username);
}
우선 도메인 클래스에 이름과 일치하는 명명된 쿼리가 있는지 검사하는 것이다. 없다면 기본적으로 메서드 이름을 기반으로 쿼리를 생성하는 전략을 사용한다. @Param 애너테이션으로는 이름 기반 파라미터를 바인딩할 수 있다.
리포지토리 메서드에 직접 쿼리를 정의하려면 org.springframework.data.jpa.repository.Query 애너테이션을 사용한다. 이는 실행할 메서드에 직접 정적 쿼리를 작성하므로 이름 없는 명명된 쿼리라고 할 수 있고, JPA 명명된 쿼리처럼 애플리케이션 실행 시점에 문법 오류를 발견할 수 있다는 장점이 있다.
예제 12.12. 메서드에 JPQL 쿼리 작성
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query("SELECT m FROM Member m WHERE m.username = ?1")
Member findByUsername(String username);
}
네이티브 SQL을 사용하려면 @Query 애너테이션의 nativeQuery 속성을 true로 설정해야 한다. 스프링 데이터 JPA가 지원하는 JPQL의 위치 기반 파라미터 바인딩은 1번부터 시작하지만 네이티브 SQL은 0번부터 시작한다.
예제 12.13. JPA 네이티브 SQL 지원
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query(value = "SELECT m FROM Member m WHERE m.username = ?0")
Member findByUsername(String username);
}
스프링 데이터 JPA는 위치 기반 파라미터 바인딩과 이름 기반 파라미터 바인딩을 모두 지원한다.
스프링 데이터 JPA의 파라미터 바인딩
SELECT m FROM Member m WHERE m.username = ?1 // 위치 기반
SELECT m FROM Member m WHERE m.username = :name // 이름 기반
기본 값은 위치 기반이며 파라미터 순서로 바인딩한다. 이름 기반 파라미터 바인딩을 사용하려면 org.springframework.data.repository.query.Param 애너테이션을 사용하면 된다. 코드 가독성과 유지보수성은 이름 기반 파라미터 바인딩이 더 우수하다.
예제 12.14. 파라미터 바인딩
import org.springframework.data.repository.query.Param;
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query("SELECT m FROM Member m WHERE m.username = :name")
Member findByUsername(@Param("name") String username);
}
예제 12.15. JPA를 사용한 벌크성 수정 쿼리
@Test
@Transactional
void bulkPriceUp() {
String qlString =
"UPDATE Product p SET p.price = p.price * 2 WHERE p.stockAmount < :stockAmount";
int resultCount = em.createQuery(qlString)
.setParameter("stockAmount", 50)
.executeUpdate();
Assertions.assertEquals(3, resultCount);
}
예제 12.16. 스프링 데이터 JPA를 사용한 벌크성 수정 쿼리
public interface MemberRepository extends JpaRepository<Member, Long> {
@Modifying
@Query("UPDATE Product p SET p.price = p.price * 2 WHERE p.stockAmount < :stockAmount")
int bulkPriceUp(@Param("stockAmount") Integer stockAmount);
}
스프링 데이터 JPA에서 벌크성 수정, 삭제 쿼리 메서드는 org.springframework.data.jpa.repository.Modifying 애너테이션으로 지정해야 한다.
만약 벌크성 쿼리 실행 직후 영속성 컨텍스트를 초기화하고 싶다면 애너테이션의 clearAutomatically 속성을 true로 설정해주면 된다. 기본 값은 false이다.
스프링 데이터 JPA의 메서드는 한 건 이상의 결과를 기대하면 컬렉션 인터페이스로, 단건을 기대하면 엔터티로 반환 타입을 지정한다.
스프링 데이터 JPA 메서드 반환 타입 지정
List<Member> findByName(String name); // 컬렉션(한 건 이상)
Member findByEmail(String email); // 엔터티(단건)
조회 결과가 없으면 컬렉션의 경우 빈 컬렉션, 엔터티는 null을 반환한다. 그리고 엔터티 반환 타입에 대해 결과가 2건 이상 조회되면 jakarta.persistence.NonUniqueResultException 예외가 발생한다.
스프링 데이터는 단건 메서드 호출 시 내부적으로 JPQL의 Query.getSingleResult() 메서드를 호출한다. 이 메서드는 원래 조회 결과가 없으면 jakarta.persistence.NoResultException 예외를 던지는데 스프링 데이터 JPA는 이를 무시하고 대신 null을 반환하도록 구현하였다.
스프링 데이터 JPA는 쿼리 메서드에 페이징과 정렬 기능을 사용할 수 있도록 다음의 2가지 파라미터를 제공한다.
org.springframework.data.domain.Sort: 정렬 기능org.springframework.data.domain.Pageable: 페이징 기능(내부적으로 Sort 포함)다음과 같이 파라미터에 Pageable을 사용하면 반환 타입으로 List 또는 org.springframework.data.domain.Page를 사용할 수 있다. 반환 타입으로 Page를 사용할 시 스프링 데이터 JPA는 페이징 기능 제공의 일환으로 검색된 데이터의 전체 건수를 조회하는 COUNT 쿼리를 추가적으로 호출한다.
예제 12.17. 페이징과 정렬 사용 예제
// COUNT 쿼리 사용
Page<Member> findByUsername(String username, Pageable pageable);
// COUNT 쿼리 사용 안 함
List<Member> findByUsername(String username, Pageable pageable);
List<Member> findByUsername(String name, Sort sort);
다음 조건으로 페이징과 정렬을 사용하는 예제 코드를 보자.
예제 12.18. Page 사용 예제 정의 코드
public interface MemberRepository extends JpaRepository<Member, Long> {
Page<Member> findByUsernameStartingWith(String username, Pageable pageable);
}
예제 12.19. Page 사용 예제 실행 코드
@Test
@Transactional
void pagination() {
// 페이징 조건과 정렬 조건 설정
PageRequest pageRequest =
PageRequest.of(0, 10, Sort.by(Sort.Direction.DESC, "username"));
Page<Member> result =
memberRepository.findByUsernameStartingWith("김", pageRequest);
List<Member> members = result.getContent();
int totalPages = result.getTotalPages();
boolean hasNextPage = result.hasNext();
Assertions.assertEquals(1, members.size());
Assertions.assertEquals(1, totalPages);
Assertions.assertFalse(hasNextPage);
}
Pageable은 인터페이스이기 때문에 실제 사용 시에는 구현체인 org.springframework.data.domain.PageRequest 객체를 사용한다. PageRequest.of() 메서드의 첫 번째 인자로는 현재 페이지, 두 번째 인자로는 조회할 데이터 개수를 입력한다. 추가적으로 정렬 정보를 인자로 전달하는 것이 가능하다.
Page 인터페이스는 다음과 같이 다양한 메서드를 제공한다.
예제 12.20. Page 인터페이스
public interface Page<T> extends Slice<T> {
static <T> Page<T> empty() {
return empty(Pageable.unpaged());
}
static <T> Page<T> empty(Pageable pageable) {
return new PageImpl(Collections.emptyList(), pageable, 0L);
}
int getTotalPages();
long getTotalElements();
<U> Page<U> map(Function<? super T, ? extends U> converter);
}
public interface Slice<T> extends Streamable<T> {
int getNumber();
int getSize();
int getNumberOfElements();
List<T> getContent();
boolean hasContent();
Sort getSort();
boolean isFirst();
boolean isLast();
boolean hasNext();
boolean hasPrevious();
default Pageable getPageable() {
return PageRequest.of(this.getNumber(), this.getSize(), this.getSort());
}
Pageable nextPageable();
Pageable previousPageable();
<U> Slice<U> map(Function<? super T, ? extends U> converter);
default Pageable nextOrLastPageable() {
return this.hasNext() ? this.nextPageable() : this.getPageable();
}
default Pageable previousOrFirstPageable() {
return this.hasPrevious() ? this.previousPageable() : this.getPageable();
}
}
JPA 쿼리 힌트는 org.springframework.data.jpa.repository.QueryHints 애너테이션으로 사용할 수 있다. 이는 SQL 힌트가 아닌 JPA 구현체에 제공하는 힌트이다.
JPA 쿼리 힌트
@QueryHints(value = { @QueryHint(name = "org.hibernate.readOnly", value = "true") }, forCounting = true)
Page<Member> findByName(String name, Pageable pageable);
forCounting 속성은 반환 타입으로 Page 인터페이스 적용 시 추가로 호출하는 페이징을 위한 COUNT 쿼리에도 쿼리 힌트를 적용할지를 설정하는 옵션이다. 기본 값은 true이다.
쿼리 시 락을 설정하려면 org.springframework.data.jpa.repository.Lock 애너테이션을 사용하면 된다.
락 설정
@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findByName(String name);
스프링 데이터 JPA로 리포지토리 개발 시 인터페이스만 정의하고 구현체는 정의하지 않는다. 하지만 메서드를 직접 구현해야 할 때 스프링 데이터 JPA는 공통 인터페이스의 기능을 모두 구현할 필요 없이 필요한 메서드만 구현하는 방법을 제공한다.
우선 다음과 같이 직접 구현할 메서드의 명세가 포함된 사용자 정의 인터페이스를 작성한다.
예제 12.25. 사용자 정의 인터페이스
public interface MemberRepositoryCustom {
public List<Member> findMemberCustom();
}
그리고 리포지토리 인터페이스 이름 + Impl 이름으로 구현 클래스를 정의하면 스프링 데이터 JPA가 이를 인식할 수 있다.
예제 12.26. 사용자 정의 구현 클래스
public class MemberRepositoryCustomImpl implements MemberRepositoryCustom {
@Override
public List<Member> findMemberCustom() {
// 사용자 정의 구현
return null;
}
}
마지막으로는 리포지토리 인터페이스에서 사용자 정의 인터페이스를 상속받으면 된다.
예제 12.27. 사용자 정의 인터페이스 상속
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
}
스프링 데이터가 제공하는 Web 확장 기능을 활성화하려면 org.springframework.data.web.config.SpringDataWebConfiguration을 스프링 빈으로 등록하면 된다.
XML 빈 등록
<bean class="org.springframework.data.web.config.SpringDataWebConfiguration" />
JavaConfig 방식을 사용할 시에는 org.springframework.data.web.config.EnableSpringDataWebSupport 애너테이션을 사용하면 된다.
JavaConfig 빈 등록
@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
public class WebAppConfig {
...
}
설정 완료 시 도메인 클래스 컨버터, 페이징과 정렬을 위한 HandlerMethodArgumentResolver 클래스 객체가 스프링 빈으로 등록된다. 도메인 클래스 컨버터로는 org.springframework.data.repository.support.DomainClassConverter가 등록된다.
스프링 부트에서 spring-boot-starter-data-jpa 의존성 스타터를 사용한다면 이러한 설정이 자동적으로 제공된다.
도메인 클래스 컨버터는 HTTP 요청 시 전달된 엔터티의 식별자로 엔터티 객체를 찾아 바인딩해주는 기능이다.
예를 들어 /member/memberUpdateForm?id=1와 같은 경로로 요청이 들어왔다고 가정해 보자.
예제 12.28. 회원의 아이디로 회원 엔터티 조회
@Controller
public class MemberController {
private final MemberRepository memberRepository;
public MemberController(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@RequestMapping("member/memberUpdateForm")
public String memberUpdateForm(@RequestParam("id") Long id, Model model) {
Member member = memberRepository.findById(id).orElse(null);
model.addAttribute("member", member);
return "member/memberSaveForm";
}
}
위 코드는 스프링 MVC에서 요청 파라미터를 통해 회원의 식별자를 받아 데이터베이스에서 조회한 후 모델에 등록한다.
예제 12.29. 도메인 클래스 컨버터 적용
@Controller
public class MemberController {
@RequestMapping("member/memberUpdateForm")
public String memberUpdateForm(@RequestParam("id") Member member, Model model) {
model.addAttribute("member", member);
return "member/memberSaveForm";
}
}
@RequestParam("id") Member member 와 같이 HTTP 요청에서 회원의 식별자를 전달받더라도 도메인 클래스 컨버터가 컨트롤러 메서드의 파라미터 바인딩 단계에서 흐름을 가로채어 식별자를 회원 엔터티 객체로 변환하여 넘겨준다. 그래서 코드가 훨씬 단순해진다.
도메인 클래스 컨버터는 해당 엔터티와 관련된 리포지토리를 사용하여 엔터티를 찾는다. 위 코드에서는 그러므로 MemberRepository 구현체를 활용한 것이다.
그리고 조회한 엔터티는 준영속 상태이기 때문에 변경하더라도 변경 감지 기능이 동작하지는 않는다.
스프링 데이터가 제공하는 페이징과 정렬 기능을 스프링 MVC에서 편리하게 사용할 수 있도록 HandlerMethodArgumentResolver를 제공한다.
PageableHandlerMethodArgumentResolverSortHandlerMethodArgumentResolver예제 12.30. 페이징과 정렬 예제
@GetMapping("/members")
public String list(Pageable pageable, Model model) {
Page<Member> page = memberService.findMembers(pageable);
model.addAttribute("members", page.getContent());
return "members/memberList";
}
컨트롤러 메서드의 매개변수로 Pageable 구현체(PageRequest)를 전달받을 시 다음의 요청 파라미터를 조합하여 객체를 생성한다.
사용해야 할 페이징 정보가 둘 이상이면 접두사를 사용해서 구분할 수 있다. 접두사는 스프링 프레임워크가 제공하는 @Qualifier 애너테이션을 사용하여 정의하며 요청 파라미터는 "접두사_"로 구분한다.
접두사 사용
public String list(
@Qualifier("member") Pageable memberPageable,
@Qualifier("order") Pageable orderPageable,
...
) { ... }
Pageable의 기본 값은 page=0, size=20이다. 기본 값 변경이 필요할 시 @PageableDefault 애너테이션을 사용하면 된다.
Pageable 기본 값 변경
@GetMapping("/members_page")
public String list(@PageableDefault(size = 12, sort = "name", direction = Sort.Direction.DESC) Pageable pageable) {
...
}
스프링 데이터 JPA가 제공하는 공통 인터페이스의 계약은 org.springframework.data.jpa.repository.support.SimpleJpaRepository 클래스가 구현한다.
예제 12.31. SimpleJpaRepository
@Repository
@Transactional(
readOnly = true
)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
...
@Transactional
public <S extends T> S save(S entity) {
Assert.notNull(entity, "Entity must not be null");
if (this.entityInformation.isNew(entity)) {
this.entityManager.persist(entity);
return entity;
} else {
return (S)this.entityManager.merge(entity);
}
}
...
}
readOnly = true 옵션을 적용하면 플러시를 생략하므로 약간의 성능 향상을 기대할 수 있다.persist)하고, 이미 존재하는 엔터티라면 병합(merge)한다. 새로운 엔터티임을 판단하는 기본 전략은 식별자 값으로, 객체일 시 null, 자바 기본 타입일 시 0이면 새로운 엔터티로 판단한다. 새로운 엔터티 판단 전략은 Persistable 인터페이스를 구현해서 변경할 수 있다.예제 12.32. Persistable
public interface Persistable<ID> {
@Nullable
ID getId();
boolean isNew();
}
스프링 데이터 JPA는 2가지 방법으로 QueryDSL을 지원한다.
org.springframework.data.querydsl.QueryDslPredicateExecutororg.springframework.data.querydsl.QueryDslRepositorySupport첫 번째 방법은 QueryDslPredicateExecutor를 리포지토리 계층에서 상속받는 것이다.
QueryDslPredicateExecutor 상속
public interface ItemRepository
extends JpaRepository<Item, Long>, QuerydslPredicateExecutor<Item> {
}
이제 상품 리포지토리에서 QueryDSL을 사용할 수 있다. 다음 예제는 toy라는 이름을 포함하며 가격이 10,000~20,000 사이인 상품을 조회한다.
예제 12.45. QueryDSL 사용 예제
public List<Item> findToysBetween(Integer lowerPrice, Integer upperPrice) {
QItem item = QItem.item;
Iterable<Item> result = itemRepository.findAll(
item.name.contains("toy").and(item.price.between(10000, 20000))
);
return StreamSupport.stream(result.spliterator(), false)
.collect(Collectors.toList());
}
이 방식은 List 타입으로 변환 시 스트림 API를 활용해야 하기에 다소 비효율적이다.
예제 12.46. QueryDslPredicateExecutor 인터페이스
public interface QuerydslPredicateExecutor<T> {
Optional<T> findOne(Predicate predicate);
Iterable<T> findAll(Predicate predicate);
Iterable<T> findAll(Predicate predicate, Sort sort);
Iterable<T> findAll(Predicate predicate, OrderSpecifier<?>... orders);
Iterable<T> findAll(OrderSpecifier<?>... orders);
Page<T> findAll(Predicate predicate, Pageable pageable);
long count(Predicate predicate);
...
}
QueryDslPredicateExecutor 인터페이스는 QueryDSL을 검색 조건으로 사용하면서 스프링 데이터 JPA가 제공하는 페이징과 정렬 기능도 함께 사용할 수 있도록 계약을 정의하고 있다. 하지만 join, fetch를 명시적으로 사용할 수 없다는 한계점도 있다. 그러므로 QueryDSL이 제공하는 다양한 기능을 사용하려면 JPAQuery 객체를 직접 사용하거나 스프링 데이터 JPA가 제공하는 QueryDslRepositorySupport를 사용해야 한다.
스프링 데이터 JPA가 제공하는 QueryDslRepsitorySupport 클래스를 상속받아 사용하면 조금 더 편리하게 QueryDSL을 사용할 수 있다.
예제 12.47. CustomOrderRepository 사용자 정의 리포지토리
public interface CustomOrderRepository {
public List<Order> search(OrderSearch orderSearch);
}
예제 12.48. QueryDslRepositorySupport 사용 코드
public class CustomOrderRepositoryImpl extends QuerydslRepositorySupport implements CustomOrderRepository {
public CustomOrderRepositoryImpl() {
super(Order.class);
}
@Override
public List<Order> search(OrderSearch orderSearch) {
QOrder order = QOrder.order;
QMember member = QMember.member;
JPQLQuery<Order> query = from(order);
if (StringUtils.hasText(orderSearch.getMemberName())) {
query.leftJoin(order.member, member)
.where(member.username.contains(orderSearch.getMemberName()));
}
if (orderSearch.getOrderStatus() != null) {
query.where(order.status.eq(orderSearch.getOrderStatus()));
}
return query.fetch();
}
}
생성자에서 super(Order.class)와 같이 QueryDslRepositorySupport 클래스에 엔터티 클래스 정보를 넘겨주어야 한다.
예제 12.49. QueryDslRepositorySupport 코드
@Repository
public abstract class QuerydslRepositorySupport {
private final PathBuilder<?> builder;
@Nullable
private EntityManager entityManager;
@Nullable
private Querydsl querydsl;
public QuerydslRepositorySupport(Class<?> domainClass) {
Assert.notNull(domainClass, "Domain class must not be null");
this.builder = (new PathBuilderFactory()).create(domainClass);
}
@Autowired
public void setEntityManager(EntityManager entityManager) {
Assert.notNull(entityManager, "EntityManager must not be null");
this.querydsl = new Querydsl(entityManager, this.builder);
this.entityManager = entityManager;
}
@PostConstruct
public void validate() {
Assert.notNull(this.entityManager, "EntityManager must not be null");
Assert.notNull(this.querydsl, "Querydsl must not be null");
}
@Nullable
protected EntityManager getEntityManager() {
return this.entityManager;
}
protected JPQLQuery<Object> from(EntityPath<?>... paths) {
return this.getRequiredQuerydsl().createQuery(paths);
}
protected <T> JPQLQuery<T> from(EntityPath<T> path) {
return this.getRequiredQuerydsl().createQuery(new EntityPath[]{path}).select(path);
}
protected DeleteClause<JPADeleteClause> delete(EntityPath<?> path) {
return new JPADeleteClause(this.getRequiredEntityManager(), path);
}
protected UpdateClause<JPAUpdateClause> update(EntityPath<?> path) {
return new JPAUpdateClause(this.getRequiredEntityManager(), path);
}
protected <T> PathBuilder<T> getBuilder() {
return this.builder;
}
@Nullable
protected Querydsl getQuerydsl() {
return this.querydsl;
}
private Querydsl getRequiredQuerydsl() {
if (this.querydsl == null) {
throw new IllegalStateException("Querydsl is null");
} else {
return this.querydsl;
}
}
private EntityManager getRequiredEntityManager() {
if (this.entityManager == null) {
throw new IllegalStateException("EntityManager is null");
} else {
return this.entityManager;
}
}
}