대부분의 데이터 접근 계층 Data Access Layer 은 CRUD 코드를 반복해서 개발해야 한다. JPA를 사용해서 데이터 접근 계층을 개발할 때도 이 같은 문제가 발생한다.
이런 문제를 해결하려면 제네릭과 상속을 적절히 사용해 공통 부분을 처리하는 부모 클래스를 만들면 된다.
스프링 데이터 JPA 소개
스프링 데이터 JPA는 스프링 프레임워크에서 JPA를 편리하게 사용할 수 있도록 지원하는 프로젝트이다. 이 프로젝트는 데이터 접근 계층을 개발할 때 지루하게 반복되는 CRUD 문제를 세련된 방법으로 해결한다.
우선 CRUD를 처리하기 위한 공통 인터페이스를 제공한다. 그리고 리포지토리를 개발할 때, 인터페이스만 작성하면 실행시점에 스프링 데이터 JPA가 구현 객체를 동적으로 생성해서 주입해준다. 따라서, 데이터 접근 계층을 개발할 때 구현 클래스 없이 인터페이스만 작성해도 개발을 완료할 수 있다.
이제 상품, 회원 리포지토리를 아래와 같이 인터페이스만 작성하면 된다.
public interface MemberRepository extends JPARepository<Member, Long>{
Member findByUsername(String name);
}
public interface ItemRepository extends JPARepository<Item, Long>{
}
findByUsername()
메소드와 같이 직접 작성한 메소드도 스프링 데이터 JPA가 메소드 이름을 분석해 적절한 JPQL을 실행한다.
select m from Member m where username = :name
스프링 데이터 프로젝트
스프링 데이터 JPA는 스프링 데이터 프로젝트의 하위 프로젝트이다.
스프링 데이터 프로젝트는 JPA, Mongo DB, NEO4J, REDIS, HADOOP 같은 다양한 데이터 저장소에 대한 접근을 추상화해서 개발자 편의를 제공하고 지루하게 반복되는 데이터 접근 코드를 줄여준다.
스프링 데이터 JPA 설정
<!-- 스프링 데이터 JPA -->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>[원하는 버전]</version>
</dependency>
라이브러리 추가 후 스프링 설정 정보를 추가하면 된다.
XML을 사용하면 아래와 같이 <jpa:repositories>
를 사용하고 base-package에 패키지를 적용한다.
<jpa:repositories base-package="{패키지 경로}"/>
JavaConfig를 사용하면 아래와 같이 @EnableJpaRepositories
를 사용하고 basePackage에 경로를 적어준다.
@Configuration
@EnableJpaRepositories(basePackage="{패키지 경로}")
public class AppConfig{
//...
}
설정 정보를 추가하면 애플리케이션을 실행할 때, base package에 있는 리포지토리 인터페이스들을 찾아 해당 인터페이스를 구현한 클래스를 동적으로 생성해서 스프링 빈으로 등록한다.
공통 인터페이스 기능
스프링 데이터 JPA를 사용하는 가장 단순한 방법은 JPARepository를 상속받은 인터페이스를 사용하는 것이다.
그림을 살펴보면 JPARepository는 스프링 데이터 프로젝트의 세 인터페이스를 모두 상속받는다. JPARepository는 공통 기능외에도 JPA에 특화된 기능을 제공한다.
save()
메서드는 식별자 값으로 신규 엔티티 여부를 판단해 신규 엔티티는 em.persist()
를, 기존 엔티티는 em.merge()
를 사용해 저장한다. 필요하다면신규 엔티티 판단 전략을 변경할 수 있다.
쿼리 메소드 기능
인터페이스에 메소드 이름을 선언하면 해당 메소드 이름으로 적절한 JPQL을 생성해 실행한다.
JPA가 제공하는 쿼리 메소드 기능은 크게 3가지가 있다.
- 메소드 이름으로 쿼리 생성
- 메소드 이름으로 JPA Named 쿼리 실행
@Query
를 사용해 실행할 쿼리 직접 정의
메소드 이름으로 쿼리 생성
JPA Query Methods :: Spring Data JPA
정해진 규칙이 있어 해당 링크를 참조해서 메소드명을 선언하면 원하는 쿼리를 생성할 수 있다.
이 방법을 사용할 경우 엔티티 속성명이 바뀌면 인터페이스 메소드명도 변경해야 한다. 그렇지 않으면 애플리케이션 실행 시점에서 오류가 발생한다.
JPA NamedQuery
스프링 데이터 JPA는 메소드 이름으로 네임드 쿼리를 호출하는 기능도 제공한다.
네임드 쿼리란 이름 그대로 쿼리에 이름을 부여해서 사용하는 방법인데, 어노테이션이나 XML에 정의할 수 있다.
public class MemberRepository{
public List<Member> findByUsername(String username){
...
List<Member> resultList =
em.creatQuery("Member.findByUsername", Member.class)
.setParameter("username", username)
.getResultList();
}
}
XML이나 어노테이션으로 네임드 쿼리를 작성 후 위와 같이 호출할 수 있는데 이 때 스프링 데이터 JPA를 사용하면 메소드 이름만으로 Named 쿼리를 호출할 수 있다.
public interface MemberRepository extends JPARepository<Member, Long>{ // 여기서 선언한 Member 도메인 클래스
List<Member> findByUsername(@Param("username") String username);
}
스프링 데이터 JPA는 우선 리포지토리의 도메인 + .
+ 메소드명으로 먼저 Named 쿼리를 찾고 없으면 해당 메소드로 쿼리를 생성 전략을 사용한다. 이 전략은 바꿀 수 있다.
@Query
, 리포지토리 메서드에 쿼리 정의
public interface MemberRepository extends JPARepository<Member, Long>{
@Query("select m from Member m where m.username = ?1")
Member findByUsername(String username);
}
위와 같이 @Query
어노테이션을 사용해 쿼리를 직접 작성할 수 있다. 또한, JPA NamedQuery와 같이 애플리케이션 실행 시점에 문법 오류를 발견할 수 있다는 장점이 있다.
네이티브 SQL을 사용하려면, @Query
속성에 nativeQuery를 true
로 주면 된다. 참고로 스프링 데이터 JPA가 제공하는 위치 기반 파라미터 바인딩을 사용하면 인덱스가 1부터 시작하지만, 네이티브 SQL은 0부터 시작한다.
파라미터 바인딩
스프링 데이터 JPA는 위치 기반 파라미터와 이름 기반 파라미터 모두 지원한다. 가독성과 유지보수 모두를 위해 이름 기반 파라미터를 사용하자
벌크성 수정 쿼리
int bulkPriceUp(int stockAmount){
String qlString =
"update Product p set p.price = p.price * 1.1 where "
+ "p.stockAmount < :stockAmount";
int resultCount = em.createQuery(qlString)
.setParameter("stockAmount", stockAmount)
.executeUpdate();
}
위와 같은 벌크 연산 메소드를 아래와 같이 바꿀 수 있다.
@Modifying
@Query("update Product p set p.price = p.price * 1.1 where p.stockAmount < :stockAmount")
int bulkPriceUp(@Param("stockAmount") int stockAmount);
스프링 데이터 JPA에서 벌크성 수정, 삭제 쿼리는 @Modifying
어노테이션을 사용하면 된다. @Modifying(clearAutomatically = true)
로 영속성 컨텍스트를 초기화할 수 있다.
벌크성 수정, 삭제 쿼리는 영속성 컨텍스트를 거치지 않고 데이터베이스에 직접 전송하므로 영속성 컨텍스트를 초기화하지 않으면 데이터의 불일치가 일어날 수 있다. 따라서, 벌크성 쿼리를 실행한 뒤에는 영속성 컨텍스트를 초기화하고 추후 필요할 때 다시 조회해서 사용하는 것이 좋다.
반환 타입
스프링 데이터 JPA는 유연한 반환 타입을 지원한다. 결과가 한 건 이상이면 컬렉션 인터페이스를 사용하고, 단 건이면 반환 타입을 지정한다.
만약 조회 결과가 없으면 컬렉션은 빈 컬렉션을, 단 건은 null을 반환한다.
단 건을 기대하고 조회했는데 두 건 이상의 결과가 나오면 NonUniqueResultException
이 발생한다. 단 건으로 지정하면 JPA는 내부적으로 getSingleResult()
를 실행하는데 이 때 결과가 없으면 NoResultException
이 발생한다. JPA는 이를 무시하고 null
로 반환한다.
페이징과 정렬
스프링 데이터 JPA는 페이징과 정렬이 가능하도록 두 가지 파라미터를 제공한다.
org.springframework.data.domain.Sort
: 정렬 기능org.springframework.data.domain.Pageable
: 페이징 기능(내부에 Sort 포함)
// count 쿼리 사용
Page<Member> findByName(String name, Pageable pageable);
// count 쿼리 미사용
List<Member> findByName(String name, Pageable pageable);
List<Member> findByName(String name, Sort sort);
반환 타입으로는 List와 Page가 있는데 Page를 사용할 경우 페이징 기능을 제공하기 위해 검색된 전체 데이터 건수를 조회하는 count
쿼리를 추가 호출한다.
// 검색 조건 : 이름이 김으로 시작
// 정렬 조건 : 이름 내림차순
// 페이징 조건 : 첫 번째 페이지, 페이지 당 10건의 데이터
PageRequest pageRequest =
new PagetRequest(0, 10, new Sort(Direction.DESC, "name"));
Page<Member> result =
memberRepository.findByNameStartingWith("김", pageRequest);
List<Member> members = result.getContent(); // 조회된 데이터
int totalPages = result.getTotalPages(); // 전체 페이지 수
boolean hasNextPage = result.hasNextPage(); // 다음 페이지 존재 여부
참고로 페이지는 0부터 시작한다. 이 외에도 Page 인터페이스를 살펴보면 다양한 메소드들을 제공한다.
힌트
JPA 쿼리 힌트를 사용하려면 @QueryHints
어노테이션을 사용하면 된다. 이것은 JPA 구현체에게 제공하는 힌트다.
@QueryHints(
value = { @QueryHint(name = "org.hibernate.readOnly",value = "true")},
forCounting = true
)
Page<Member> findByName(String name, Pageble pageale);
forCounting 속성은 반환 타입으로 Page 인터페이스를 적용하면 추가로 호출하는 페이징을 위한 count 쿼리에도 쿼리 힌트를 적용할 지 설정하는 옵션이다. 기본값은 true이다.
[DATA JPA] JPA Hint 사용하여 select 쿼리 최적화하기
쿼리 힌트에서 읽기 전용 속성을 true로 설정하면 영속성 컨텍스트가 스냅샷을 저장하지 않아 성능 향상을 기대할 수 있다.
Lock
쿼리시 락을 걸려면 Lock
어노테이션을 사용하면 된다. Lock에 대해서는 추후 설명 예정이다.
명세
책 도메인 주도 설계(Domain Driven Design)는 명세(SPECIFICATION)라는 개념을 소개한다. 스프링 데이터 JPA는 JPA Criteria로 이 개념을 사용할 수 있도록 지원한다.
명세를 이해하기 위한 핵심 단어는 술어(predicate)인데 이것은 단순히 참이나 거짓으로 평가된다. 그리고 이것은 AND나 OR같은 연산자로 조합할 수 있다. 이 술어를 스프링 데이터 JPA에서는 Specification 클래스로 정의했고 이 클래스는 컴포지트 패턴으로 구성되어 있어 여러 검색 조건을 조립해서 새로운 검색 조건을 만들 수 있다.
명세 기능을 사용하려면 JpaSpecificationExecutor 인터페이스를 상속받으면 된다.
위 메소드들은 Specification을 파라미터로 받아 검색 조건으로 사용한다.
import static org.springframework.data.jpa.domain.Specifications.*; // where()
import static [내 Specification 구현체 위치]
public List<Order> findOrders(String name){
List<Order> results = orderRepository.findAll(
where(memberName(name)).and(isOrderStatus())
);
return results;
Specifications는 명세들을 조립할 수 있게 도와주는 클래스이다. where()
, and()
, or()
, not()
과 같은 메소드를 제공한다.
package jpabook.jpashop.domain;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.JoinType;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.util.StringUtils;
public class OrderSpec {
public static Specification<Order> memberNameLike(final String memberName){
return (root, query, builder) -> {
if (StringUtils.isEmpty(memberName)) return null;
Join<Order, Member> m =
root.join("member", JoinType.INNER); // 회원과 조인
return builder.like(m.<String>get("name"), "%" + memberName + "%");
};
}
public static Specification<Order> orderStatusEq(final OrderStatus orderStatus){
return (root, query, builder) -> {
if(orderStatus == null) return null;
return builder.equal(root.get("status"), orderStatus);
};
}
}
명세를 정의하려면 Specification
인터페이스를 구현하면 된다. 여기서는 편의상 람다식을 활용했다. 명세를 정의할 때는 toPredicate(...)
메소드만 구현하면 되는데 JPA Criteria의 Root, CriteriaQuery, CriteriaBuilder 클래스가 모두 파라미터로 주어진다. 이 파라미터를 활용해 적절한 검색조건을 반환하면 된다.
사용자 정의 리포지토리 구현
스프링 데이터 JPA로 리포지토리를 개발하면 인터페이스로 개발하게 된다. 하지만, 특정 메소드를 직접 구현해야할 때가 있다. 이 때, 리포지토리를 직접 구현하면 공통 인터페이스가 제공하는 기능까지 모두 구현해야 한다. 스프링 데이터 JPA는 이런 문제를 우회해서 필요한 메소드만 구현하는 방법을 제공한다.
먼저 직접 구현할 메소드를 위한 사용자 정의 인터페이스를 작성해야 한다.
public interface MemberRepositoryCustom{
public List<Member> findMemberCustom();
}
다음으로 아래와 같이 사용자 정의 인터페이스를 구현한 클래스를 작성해야 한다. 이 때 클래스는 리포지토리 인터페이스 이름 + Impl
로 지어야 한다. 스프링 데이터 JPA가 이를 사용자 정의 구현 클래스로 인식한다.
public class MemberRepositoryImpl implements MemberRepositoryCustom{
@Override
public List<Member> findMemberCustom(){
// 직접 구현
}
}
마지막으로 아래와 같이 리포지토리 인터페이스에서 사용자 정의 인터페이스를 상속받으면 된다.
public interface MemberRepository
extends JPARepository<Member, Long>, MemberRepositoryCustom{
}
만약 사용자 정의 구현 클래스에서 이름 끝에 Impl
대신 다른 이름을 붙이고 싶으면 repository-impl-postfix
속성을 변경하면 된다. 기본값은 Impl
이다.
<repositories base-package="[리포지토리 경로]"
repository-impl-postfix="[원하는 접미사]">
@EnableJpaRepositories(basePackages = "[리포지토리 경로]",
repositoryImplementationPostfix = "Impl")
Web 확장
스프링 데이터 JPA는 스프링 MVC에서 사용하기 편한 기능을 추가로 제공한다. 도메인 클래스를 식별자로 바로 바인딩 해주는 도메인 컨버터 기능과 페이징과 정렬기능이다.
설정
스프링 데이터가 제공하는 Web 확장 기능을 사용하려면 org.springframework.data.web.config.SpringDataWebConfiguration
을 스프링 빈으로 추가하면 된다.
xml에서는 아래와 같이
<bean class="org.springframework.data.web.config.SpringDataWebConfiguration" />
JavaConfig에서는 @EnableSpringDataWebSupport
를 추가하면 된다.
설정을 완료하면 도메인 클래스 컨버터와 페이징, 정렬을 위한 HandlerMethodArgumentResolver
가 스프링 빈으로 등록된다. 등록되는 도메인 컨버터는 다음과 같다.
org.springframework.data.repository.support.DomainClassConverter
도메인 클래스 컨버터 기능
도메인 클래스 컨버터는 HTTP 파라미터로 넘어온 엔티티의 아이디로 엔티티를 찾아서 바인딩해준다.
@Controller
public class MemberController{
@Autowired MemberRepository memberRepository;
@RequestMapping("member/memberUpdateForm")
public String memberUpdateForm(@RequestParam("id") Long id, Model model){
Member member = memberRepository.findOne(id);
model.addAttribute("member", member);
return "member/memberSaveForm";
}
}
위와 같이 회원을 수정할 때 넘겨받은 아이디로 회원을 조회한 후 모델에 넘겨준다. 도메인 클래스 컨버터를 사용하면 아래와 같이 사용할 수 있다.
@Controller
public class MemberController{
@Autowired MemberRepository memberRepository;
@RequestMapping("member/memberUpdateForm")
public String memberUpdateForm(@RequestParam("id") Member member, Model model){
model.addAttribute("member", member);
return "member/memberSaveForm";
}
}
받아올 때는 id를 받아오지만 도메인 클래스 컨버터가 동작해서 회원 엔티티 객체로 변환해서 넘겨준다. 따라서, 컨트롤러를 단순하게 사용할 수 있다. 참고로, 도메인 클래스 컨버터는 해당 엔티티와 연관된 리포지토리를 사용해서 엔티티를 찾는다.
도메인 클래스 컨버터를 통해 넘어온 회원 엔티티를 컨트롤러에서 직접 수정해도 실제 데이터베이스에 반영되지 않는다. 이것은 순전히 영속성 컨텍스트의 동작방식과 OSIV와 관련이 있다.
OSIV 미사용 : 조회한 엔티티는 준영속 상태이다. 따라서 변경 감지 기능이 동작하지 않는다. 수정한 내용을 반여아고 싶다면 merge를 사용해야 한다.
OSIV 사용 : 조회한 엔티티는 영속 상태다. 그러나 OSIV 특성상 컨트롤러와 뷰에서 영속성 컨텍스트를 플러시하지 않는다. 따라서, 수정한 내용을 데이터베이스에 반영하고 싶다면 트랜잭션을 시작하는 서비스 계층에서 호출해야 한다. 해당 서비스 계층이 종료될 때 플러시와 트랜잭션 커밋이 일어나서 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영해줄 것이다.
페이징과 정렬 기능
- 페이징 기능 :
PageableHandlerMethodArgumentResolver
- 정렬 기능 :
SortHandlerMethodArgumentResolver
@RequestMapping(value = "/members", method = RequestMethod.GET)
public String list(Pageable pageable, Model model){
Page<Member> page = memberService.findMembers(pageable);
model.addAttribute("members", page.getContent());
return "members/memberList";
}
파라미터로 Pageable을 받는데 이는 다음 요청 파라미터 정보로 만들어진다.(Pageable은 인터페이스이므로 실제로는 PageRequest 객체가 생성된다)
- page : 현재 페이지, 0부터 시작
- size : 한 페이지에 노출할 데이터 건수
- sort : 정렬 조건 정의
ex) /members?page=0&size=20&sort=name,desc&sort=address.city
접두사
사용해야할 페이징 정보가 둘 이상이라면 접두사를 사용해 구분할 수 있다. @Qulifier("접두사명")
어노테이션을 사용한다.
public String list(
@Qualifier("member") Pageable memberPageable,
@Qualifier("order") Pageable orderPageable, ...
기본값
Pageable의 기본값은 page(0), size(20)이다. 만약 기본값을 변경하고 싶다면 @PageableDefault
어노테이션을 사용하면 된다.
@RequestMapping(value = "/member_page", method = RequestMethod.GET)
public String list(@PageableDefault(size = 12, sort = "name",
direction = Sort.Direction.DESC) Pageable pageable) {
...
}
스프링 데이터 JPA가 사용하는 구현체
스프링 데이터가 JPA가 제공하는 공통 인터페이스는 SimpleJpaRepository 가 구현한다. 코드 일부를 살펴보자.
@Repository
@Transactional(
readOnly = true
)
public class SimpleJpaRepository<T, ID extends Serializable> implements JpaRepository<T, ID>, JpaSpecificationExecutor<T> {
@Transactional
public <S extends T> S save(S entity) {
if (this.entityInformation.isNew(entity)) {
this.em.persist(entity);
return entity;
} else {
return this.em.merge(entity);
}
}
}
- @Transactional 트랜잭션 적용
- JPA의 모든 변경은 트랜잭션 안에서 이루어져야 한다. 스프링 데이터 JPA가 제공하는 공통 인터페이스를 사용하면 변경하는 메소드에 @Transactional로 트랜잭션 처리가 되어있다. 따라서, 서비스 계층에서 트랜잭션을 사용하지 않으면 리포지토리에서 트랜잭션을 시작한다. 서비스 계층에서 트랜잭션을 사용했다면 리포지토리도 해당 트랜잭션을 전파받아 사용한다.
- @Transactional(readOnly = true)
- 데이터를 조회하는 메소드에는
readOnly
옵션이 true로 설정되어 있다. 데이터를 변경하지 않는 트랜잭션에서 해당옵션을 true로 두면 플러시를 생략해서 약간의 성능 향상을 얻을 수 있다. - save() 메소드
- 필요하면 신규 엔티티 판단 전략을 Persistable 인터페이스를 구현해서 변경할 수 있다.
예제 적용
package jpabook.jpashop.repository;
import jpabook.jpashop.domain.Member;
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderSearch;
import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.*;
import java.util.ArrayList;
import java.util.List;
@Repository
public class OrderRepository {
@PersistenceContext
EntityManager em;
public void save(Order order) {
em.persist(order);
}
public Order findOne(Long id) {
return em.find(Order.class, id);
}
public List<Order> findAll(OrderSearch orderSearch) {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Order> cq = cb.createQuery(Order.class);
Root<Order> o = cq.from(Order.class);
List<Predicate> criteria = new ArrayList<Predicate>();
//주문 상태 검색
if (orderSearch.getOrderStatus() != null) {
Predicate status = cb.equal(o.get("status"), orderSearch.getOrderStatus());
criteria.add(status);
}
//회원 이름 검색
if (StringUtils.hasText(orderSearch.getMemberName())) {
Join<Order, Member> m = o.join("member", JoinType.INNER); //회원과 조인
Predicate name = cb.like(m.<String>get("name"), "%" + orderSearch.getMemberName() + "%");
criteria.add(name);
}
cq.where(cb.and(criteria.toArray(new Predicate[criteria.size()])));
TypedQuery<Order> query = em.createQuery(cq).setMaxResults(1000); //최대 검색 1000 건으로 제한
return query.getResultList();
}
}
주문 리포지토리에는 검색이라는 복잡한 로직이 있다. 이를 스프링 데이터 JPA가 제공하는 명세 기능을 사용해서 검색을 구현해보자. 명세 기능을 사용하기 위해 주문 리포지토리가 JpaSpecificationExecutor를 상속받았다.
@Repository
public interface OrderRepository extends JpaRepository<Order, Long>, JpaSpecificationExecutor<Order> {
}
명세 적용
명세를 작성하기 위한 클래스인 OrderSpec을 추가했다.
package jpabook.jpashop.domain;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.JoinType;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.util.StringUtils;
public class OrderSpec {
public static Specification<Order> memberNameLike(final String memberName){
return (root, query, builder) -> {
if (StringUtils.isEmpty(memberName)) return null;
Join<Order, Member> m =
root.join("member", JoinType.INNER); // 회원과 조인
return builder.like(m.<String>get("name"), "%" + memberName + "%");
};
}
public static Specification<Order> orderStatusEq(final OrderStatus orderStatus){
return (root, query, builder) -> {
if(orderStatus == null) return null;
return builder.equal(root.get("status"), orderStatus);
};
}
}
다음으로 검색 조건을 가지고 있는 OrderSearch 객체에 자신이 가진 검색 조건으로 Specification을 생성하도록 하는 팩토리 메서드를 추가했다.
public class OrderSearch{
private String memberName;
private OrderStatus orderstatus;
// Getter & Setter
...
public Specifications<Order> toSpecification(){
return where(memberNameLike(memberName))
.and(orderStatusEq(orderstatus));
}
}
마지막으로 기존 코드인 리포지토리의 검색 코드가 명세를 파라미터로 넘기도록 변경한다.
public List<Order> findOrders(OrderSearch orderSearch) {
return orderRepository.findAll(orderSearch.toSpecification());
}
스프링 데이터 JPA와 QueryDSL 통합
스프링 데이터 JPA는 두 가지 방법으로 QueryDSL을 지원한다.
QueryDSLPredicateExecutor 사용
첫 번째 방법은 리포지토리에서 QueryDSLPredicateExecutor
를 상속받으면 된다.
위와 같이 QueryDslPredicateExecutor 인터페이스를 살펴보면 QueryDSL을 검색 조건으로 사용하면서 스프링 데이터 JPA가 제공하는 페이징과 정렬기능도 함께 사용 가능하다.
QueryDslPredicateExecutor만을 사용해도 QueryDSL을 사용할 수 있지만 기능에 한계가 있다. 따라서, QueryDSL이 제공하는 다양한 기능을 사용하려면 JPAQuery를 직접 사용하거나 스프링 데이터 JPA가 제공하는 QueryDslRepositorySupport를 사용해야 한다.
QueryDslRepositorySupport 사용
QueryDSL의 모든 기능을 사용하려면 JPAQuery를 생성해서 사용하면 된다. 이 떄 스프링 데이터 JPA가 제공하는 QueryDslRepositorySupport를 상속받으면 좀 더 편리하게 QueryDSL을 사용할 수 있다.
package jpabook.jpashop.repository;
import java.util.List;
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderSearch;
public interface CustomOrderRepository {
public List<Order> search(OrderSearch orderSearch);
}
JPA가 제공하는 공통 인터페이스는 직접 구현할 수 없기 때문에 사용자 정의 리포지토리를 만들었다. 여기에 QueryDslRepositorySupport를 사용해보자.
package jpabook.jpashop.repository;
import com.mysema.query.jpa.JPQLQuery;
import java.util.List;
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderSearch;
import jpabook.jpashop.domain.QMember;
import jpabook.jpashop.domain.QOrder;
import org.springframework.data.jpa.repository.support.QueryDslRepositorySupport;
import org.springframework.util.StringUtils;
public class OrderRepositoryImpl extends QueryDslRepositorySupport
implements CustomOrderRepository {
// Order 엔티티 정보 QueryDslRepositorySupport로 전달
public OrderRepositoryImpl() {
super(Order.class);
}
@Override
public List<Order> search(OrderSearch orderSearch) {
QOrder order = QOrder.order;
QMember member = QMember.member;
JPQLQuery query = from(order);
if (StringUtils.hasText(orderSearch.getMemberName())) {
query.leftJoin(order.member, member)
.where(member.name.contains(orderSearch.getMemberName()));
}
if (orderSearch.getOrderStatus() != null) {
query.where(order.status.eq(orderSearch.getOrderStatus()));
}
return query.list(order);
}
}
위 코드는 이전 주문 내역 검색 기능을 QueryDslRepositorySupport를 이용해 QueryDSL로 구현한 예제다. 검색 조건에 따라 동적으로 쿼리를 생성한다. 참고로 생성자에서 QueryDslRepositorySupport에 엔티티 클래스 정보를 넘겨줘야 한다.
정리
스프링 데이터 JPA 기능을 적용한 예제를 보면 지루한 데이터 접근 계층의 코드가 상당히 많이 줄어든 것을 알 수 있다. 스프링 프레임워크와 JPA를 함께 사용한다면 스프링 데이터 JPA는 필수이다.
'JPA' 카테고리의 다른 글
13장_웹 어플리케이션과 영속성 관리 (0) | 2024.04.05 |
---|---|
11장_웹 애플리케이션 제작 (0) | 2024.04.01 |
Java ORM 표준 - JPA 프로그래밍 10장 (0) | 2024.03.24 |
Java ORM 표준 - JPA 프로그래밍 9장 (0) | 2024.01.19 |