[자바 ORM 표준 JPA 프로그래밍] 12주차 스터디

박서영·2026년 8월 14일

12.1 스프링 데이터 JPA 소개

스프링 데이터 JPA

스프링 프레임워크에서 JPA를 편리하게 사용할 수 있도록 지원하는 프로젝트.

  • CRUD를 처리하기 위한 공통 인터페이스 제공
  • 레포지토리 개발 시 → 인터페이스만 작성하면 실행 시점에서 구현 객체를 동적으로 생성해 주입. ⇒ 데이터 접근 계층 개발 시, 구현 클래스 없이 인터페이스만으로 개발 완료 가능.
public interface MemberRepository extends JpaRepository<Member, Long> {
	Member findByUsername(String username);
}
  • 일반적은 CRUD 메소드 = JpaRepository 인터페이스가 공통으로 제공
  • 직접 작성해 공통 처리가 불가능한 메소드 = 스프링 데이터 JPA가 메소드 이름을 분석해 JPQL을 실행
MemberRepository.findByUsername();
// select m from Member m where username =: username;

(1) 스프링 데이터 프로젝트

스프링 데이터 프로젝트

JPA, MongoDB, NEO4J, REDIS, HADOOP, GEMFIRE 등 다양한 데이터 저장소에 대한 접근을 추상화

  • 스프링 데이터 JPA는 스프링 데이터 프로젝트의 하위 프로젝트 중 하나로, JPA에 특화된 기능을 제공.

12.2 스프링 데이터 JPA 설정

  • 필요한 라이브러리
    <dependency>
    	<groupId>org.springframework.data</groupId>
    	<artifactId>spring-data-jpa</artifactId>
    	<version>1.8.0.RELEASE</version>
    </dependency>
  • 환경설정
    • 스프링 설정에 XML 사용 시 <jpa:repositories>를 사용하고 레포지토리를 검색할 base-package를 적음. → 해당 패키지와 그 하위 패키지를 검색함

      <?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:jpa="http://www.springframework.org/schema/data/jpa"
      	xsi:schemaLocation="http://www.sprintframework.org/schema/beans
      		http://www.springframework.org/schema/beans/spring-beans.xsd
      		http://www.springframework.org/schema/data/jpa
      		http://www.springframework.org/schema/jpa/spring-jpa.xsd">
      	
      	<jpa:repositories base-package="jpabook.jpashop.repository"/>
      </beans>
    • 스프링 설정에 JavaConfig를 사용하면 @EnableJpaRepositories 어노테이션을 추가하고 basePackages에는 레포지토리를 검색할 패키지 위치를 적음

      @Configuration
      @EnableJpaRepositories(basePackages = "jpabook.jpashop.repository")
      public class AppConfig {}

⇒ 스프링 데이터 JPA는 애플리케이션 실행 시 basePackage에 있는 레포지토리 인터페이스들을 찾아 해당 인터페이스를 구현한 클래스를 동적으로 생성한 후 스프링 빈으로 등록. 즉, 개발자가 직접 구현 클래스를 만들지 않아도 됨.


12.3 공통 인터페이스 기능

  • 스프링 데이터 JPA에서 제공하는 JpaRepository 인터페이스 사용을 위해서는 해당 인터페이스를 상속받고, 제너릭에 <엔티티 클래스, 해당 클래스가 사용하는 식별자 타입>을 지정하면 됨.
    public interface JpaRepository<T, ID extends Serializable> extends PagingAndSortingRepository<T, ID>
    
    public interface MemberRepository extends JpaRepository<Member, Long> {}
  • 위: 스프링 데이터 모듈, 그 안에 Repository, CrudRepository, PagingAndSortingRepository → 이 인터페이스는 스프링 데이터 프로젝트가 공통으로 사용하는 인터페이스.
  • T: 엔티티, ID: 엔티티의 식별자 타입, S: 엔티티와 그 자식 타입

주요 메소드

  • save(S): 새로운 엔티티는 저장하고 이미 있는 엔티티는 수정
    • 엔티티에 식별자 값이 없으면 새로운 엔티티로 판단해 EntityManager.persist()를 호출, 식별자 값이 있으면 이미 있는 엔티티로 판단해 EntityManager.merge()를 호출.
  • delete(T): 엔티티 하나를 삭제. 내부에서는 EntityManager.remove()를 호출.
  • findOne(ID): 엔티티 하나를 조회. 내부에서 EntityManager.getReference()를 호출.
  • findAll(...): 모든 엔티티를 조회

12.4 쿼리 메소드 기능

대표적인 쿼리 메소드 기능으로는 이름만으로 쿼리를 생성하는 기능이 존재. 인터페이스에 메소드만 선언하면 해당 메소드의 이름으로 적절한 JPQL을 생성해 실행함.

스프링 데이터 JPA에서 제공하는 쿼리 메소드 기능은 크게 3가지 존재.

  • 메소드 이름으로 쿼리 생성
  • 메소드 이름으로 JPA NamedQuery 호출
  • @Query 어노테이션을 사용해 레포지토리 인터페이스에 쿼리 직접 정의

(1) 메소드 이름으로 쿼리 작성

예) 이메일과 이름으로 회원 조회

pulibc interface MemberRepository extends JpaRepository<Member, Long> {
	List<Member> findByEmailAndName(String email, String name);
}
  • 위의 메소드를 실행하면 스프링 데이터 JPA는 메소드 이름을 분석해 JPQL을 생성하고 실행
  • 실행된 JPQL
    select m from Member m where m.email = ?1 and m.name = ?2;
  • 단, 정해진 규칙에 따라 메소드 이름을 지어야함.

(2) JPA NamedQuery

쿼리에 이름을 부여해 사용하는 방법으로 어노테이션 또는 XML에 쿼리를 정의할 수 있음. 같은 방법으로 Named 네이티브 쿼리 역시 지원.

@NamedQuery 어노테이션 사용

@Entity
@NamedQuery(
	name = "Member.findByUsername"
	query = "select m from Member m where m.username = :username")
public class Member {
	...
}

orm.xml의 XML 사용

<named-query name="Member.findByUsername">
	<query><CDATA[
		select m
		from Member m
		where m.username = :username
	]></query>
<named-query>

위처럼 정의한 Named 쿼리를 JPA에서 직접 호출하기 위해서는 아래처럼 코드를 작성해야함

public class MemberRepository {
	
	public List<Member> findByUsername(String username) {
		...
		List<Member> resultList =
			em.createNamedQuery("Member.findByUsername", Member.class)
			.setParameter("username", "회원1")
			.getResultList();
	}
}

스프링 데이터 JPA 사용

메소드 이름만으로 Named 쿼리 호출 가능

public interface MemberRepository extends JpaRepository<Member, Long> {
	List<Member> findByUsername(@Param("username" String username);
}
  • 스프링 데이터 JPA는 선언한 “도메인 클래스 + .(점) + 메소드이름”으로 Named 쿼리를 찾아서 실행
  • 실행할 Named 쿼리가 없으면 메소드 이름으로 쿼리 생성 전략을 사용.
  • @Param: 이름기반 파라미터를 바인딩하기 위해서 사용하는 어노테이션.

(3) @Query, 레포지토리 메소드에 쿼리 정의

레포지토리 메소드에 직접 쿼리를 정의하기 위해서 @Query 어노테이션을 사용.

정적 쿼리를 직접 작성하는 방법으로 이름 없는 Named 쿼리라 볼 수 있음. 또한, JPA Named 쿼리처럼 애플리케이션 실행 시점에 문법 오류를 발견할 수 있다는 장점 존재.

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를 설정.

public interface MemberRepository extends JpaRepository<Member, Long> {
	
	@Query(value = "SELECT * FROM MEMBER WHERE USERNAME = ?0",
		nativeQuery = true)
	Member findByUsername(String username);
}

(4) 파라미터 바인딩

스프링 데이터 JPA는 위치 기반 파라미터 바인딩과 이름 기반 파라미터 바인딩을 모두 지원함

select m from Member m where m.username = ?1 //위치기반
select m from Member m whre m.username = :name //이름기반
  • 기본값은 위치 기반으로 파라미터 순서로 바인딩.
  • 이름 기반 파라미터 바인딩 사용을 위해서는 @Param 어노테이션을 사용
public interface MemberRepository extends JpaRepository<Member, Long> {
	
	@Query("select m from Member m where m.username = :name")
	Member findByUsername(@Param("name") String username);
}

(5) 벌크성 수정 쿼리

  • JPA로 작성한 벌크성 수정 쿼리
int bulkPriceUp (String 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();
}
  • 스프링 데이터 JPA를 사용한 벌크성 수정 쿼리
    • 스프링 데이터 JPA에서 벌크성 수정, 삭제 쿼리는 @Modifying 어노테이션을 사용하면 됨.
    • 벌크성 쿼리 실행 후, 영속성 컨텍스트 초기화를 위해서는 @Modifying(clearAutomatically=true) 처럼 clearAutomatically 옵션을 true로 설정하면 됨. (기본값은 false)
public interface MemberRepository extends JpaRepository<Member, Long> {
	
	**@Modifying**
	@Query("update Product p set p.price = p.price * 1.1 "+
		"where p.stockAmount < :stockAmount")
	int bulkPriceUp(@Param("stockAmount") String stockAmount);
}

(6) 반환 타입

  • 스프링 데이터 JPA는 유연한 반환 타입을 지원
    • 결과가 한건 이상 → 컬렉션 인터페이스를 사용
    • 결과가 단건 → 반환 타입을 지정
  • 조회 결과가 없는 경우
    • 컬렉션 → 빈 컬렉션 반환
    • 단건 → null 반환
  • 단건을 기대하고 반환 타입을 지정했는데, 결과가 2건 이상 조회되는 경우에는 NonUniqueResultException 예외가 발생.

+) 단건으로 지정한 메소드 호출 시, 스프링 데이터 JPA는 내부에서 JPQL의 Query.getSingleResult() 메소드를 호출함. 조회 결과 없을 때에는 NoResultException 예외가 발생하는데 스프링 데이터 JPA에서는 이 예외 발생시 예외를 무시하고 null을 반환.

(7) 페이징과 정렬

스프링 데이터 JPA는 쿼리 메소드에 페이징과 정렬 기능을 사용할 수 있도록 2가지 특별한 파라미터를 제공함

  • Sort: 정렬 기능
  • Pageable: 페이징 기능 (내부에 Sort 포함)

Pageable을 사용하면 반환 타입으로 List나 Page를 사용할 수 있음. 반환 타입을 Page를 사용하면 스프링 데이터 JPA는 페이징 기능 제공을 위해 검색된 전체 데이터 건수를 조회하는 count 쿼리를 추가로 호출함.

///count 쿼리 사용
Page<Member> findByName(String name, Pageable pageable);

//count 쿼리 사용X
List<Member> findByName(String name, Pageable pageable);

List<Member> findByName(String name, Sort sort);

예시:

  • 검색 조건: 이름이 김으로 시작하는 회원
  • 정렬 조건: 이름으로 내림차순
  • 페이징 조건: 첫 번째 페이지, 페이지당 보여줄 데이터는 10건
public interface MemberRepository extends JpaRepository<Member, Long> {
	
	Page<Member> findByNameStartingWith(String name, Pageable Pageable);
}

//페이징 조건과 정렬 조건 설정
PageRequest pageRequest = 
	new PageRequest(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(); //다음 페이지 존재 여부
  • 여기서 Pageable은 인터페이스로 실제 사용할 때는 이 인터페이스를 구현한 PageRequest를 사용함.

PageRequest

  • 생성자의 첫 번째 파라미터 = 현재 페이지, 두 번째 파라미터 = 조회할 데이터의 수, 추가적으로 정렬 정보 역시 파라미터로 사용 가능.

Page 인터페이스의 메소드

public interface Page<T> extends Iterable<T> {

	int getNumber(); //현재 페이지
	int getSize(); //페이지 크기
	int getTotalPages(); //전체 페이지 수
	int getNumberOfElements(); //현재 페이지에 나올 데이터 수
	long getTotalElements(); //전체 데이터 수
	
	boolean hasPreviousPage(); //이전 페이지 여부
	boolean isFirstPage(); //현재 페이지가 첫번째인지의 여부
	boolean hasNextPage(); //다음 페이지 여부
	boolean isLastPage(); //마지막 페이지인지의 여부
	
	Pageable previousPageable(); //이전 페이지 객체, 없으면 null
	Pageable nextPageable(); //다음 페이지 객체, 없으면 null
	
	List<T> getContent(); //조회된 데이터
	boolean hasContent(); //조회된 데이터 존재 여부
	
	Sort getSort(); //정렬 정보
}

Pageable과 Page를 사용하면 반복적인 페이징 처리를 손쉽게 개발 가능함.

(8) 힌트

JPA 쿼리 힌트를 사용하기 위해서는 @QueryHints 어노테이션을 사용하면 됨. SQL 힌트가 아니라 JPA 구현체에게 제공하는 힌트.

@QueryHints(value = {
		@QueryHint(name = "readOnly", value ="true")}, forCounting = true)
		Page<Member> findByName(String name, Pageable pageable);
	}

forCounting 속성은 반환 타입으로 Page 인터페이스를 적용하면 추가로 호출하는 페이징을 위한 count 쿼리에도 쿼리힌트를 적용할지 설정하는 옵션 (기본값은 true)

(9) Lock

쿼리 시 락을 걸기위해서는 @Lock 어노테이션을 사용하면 됨.

@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findByName(String name);

12.5 명세

명세라는 개념을 스프링 데이터 JPA는 JPA Criteria로 해당 개념을 사용할 수 있도록 지원함.

명세: 핵심 단어는 술어로 단순히 참이나 거짓으로 평가됨. 이는 AND/OR 같은 연산자로 조합할 수 있음. 예로는 데이터 검색을 위한 제약조건 하나하나를 술어라 할 수 있음.

→ 이 술어를 Specification 클래스로 정의함.

  • 컴포지트 패턴으로 구성되어, 여러 Specification을 조합할 수 있음. → 다양한 검색 조건을 조립해 새로운 검색 조건을 쉽게 만들 수 있음.

명세 기능 사용을 위해서는 JpaSpecificationExecutor 인터페이스를 상속받으면 됨.

public interface OrderRepository extends JpaRepository<Order, Long>, JpaSpecificationExecutor<Order> {}
public interface JpaSpecificationExecutor<T> {
	
	T findOne(Specification<T> spec);
	List<T> findAll(Specification<T> spec);
	Page<T> findAll(Specification<T> spec, Pageable pageable);
	List<T> findAll(Specification<T> spec, Sort sort);
	long count(Specifiaction<T> spec);
}
  • JpaSpecificationExecutor의 메소드들은 Specification을 파라미터로 받아서 검색 조건으로 사용함.

명세 사용 예제

public List<Order> findOrders(String name) {
	
	List<Order> result = orderRepository.findAll(
		where(memberName(name)).and(isOrderStatus())
	);
	
	return result;
}
  • Specifiaction은 명세들을 조립할 수 있도록 도와주는 클래스로 where(), and(), or(), not() 메소드를 제공함.
  • 위의 findAll을 보면 회원 이름 명세(memberName)와 주문 상태 명세(isOrderStatus)를 and로 조합해 검색 조건으로 사용

OrderSpec을 정의하는 코드

  • 명세 정의를 위해서는 Specifiaction 인터페이스를 구현하면 됨.
  • 명세를 정의할 때는 toPredicate() 메소드를 구현하면 됨.
    • 이때, JPA Criteria의 Root, CriteriaQuery, CriteriaBuilder 클래스가 모두 파라미터로 주어짐.
    • 위의 파라미터들을 활용해 적절한 검색 조건을 반환하면 됨.
public class OrderSpec {
	
	public static Specification<Order> memberName(final String memberName) {
		
		return new Specification<Order>() {
			public Predicate toPredicate(Root<Order> root, CriteriaQuery<?> query, 
			CriteriaBuilder builder) {
			
				if (StringUtils.isEmpty(memberName)) return null;
				
				Join<Order, Member> m = root.join("member", JoinType.INNER); //회원과 조인
				
				return builder.equal(m.get("name"), memberName);
			}
		};
		
	}
	
	public static Specifiaction<Order> isOrderStatus() {
		
		return new Specification<Order>() {
			public Predicate toPredicate(Root<Order> root, CriteriaQuery<?> query, 
			CriteriaBuilder builder) {
			
				return builder.equal(root.get("status"), OrderStatus.ORDER);
			}
		};
	}
}

12.6 사용자 정의 레포지토리 구현

스프링 데이터 JPA로 레포지토리를 개발할 때 인터페이스의 정의만으로 가능하기도 하지만, 메소드를 직접 구현해야하는 경우 역시 존재. 이때 공통 인터페이스가 제공하는 기능까지 모두 구현을 해야하는데, 스프링 데이터 JPA에서 해당 문제를 우회해 필요한 메소드만 구현할 수 있는 방법을 제공함.

1. 사용자 정의 인터페이스 작성

직접 구현할 메소드를 위한 사용자 정의 인터페이스를 작성. 이때 이름은 자유.

public interface MemberRepositoryCustom {
	public List<Member> findMemberCustom();
}

2. 사용자 정의 인터페이스 구현 클래스 작성

이때는 클래스 이름을 짓는 규칙 존재 → “레포지토리 인터페이스 이름 + Impl”로 지어야함. 이렇게 해야 스프링 데이터 JPA가 사용자 정의 구현 클래스로 인식함.

  • 사용자 정의 구현 클래스 이름 끝에 Impl 대신 다른 이름을 붙이려면 repository-impl-postfix 속성을 변경하면 됨. (기본값은 Impl)
    • JavaConfig 설정: @EnableJpaRepositories(basePackages = "jpabook.jpashop.repository", repositoryImplementationPostfix = "Impl")
public class MemberRepositoryImpl implements MemberRepositoryCustom {
	
	@Override
	public List<Member> findMemberCustom() {
		..//사용자 정의 구현
	}
}

3. 레포지토리 인터페이스에서 사용자 정의 인터페이스 상속

public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {

}

12.7 Web 확장

스프링 데이터 프로젝트는 스프링 MVC에서 사용 가능한 편리한 기능을 제공. 식별자로 도메인 클래스를 바로 바인딩해주는 도메인 클래스 컨버터 기능, 페이징과 정렬 기능 존재.

(1) 설정

스프링 데이터가 제공하는 Web 확장 기능을 활성화하기 위해서는 SpringDataWebConfiguration을 스프링 빈으로 등록해야함

<bean class="org.springframework.data.web.config.SpringDataWebConfiguration" />

JavaConfig를 사용할 경우 아래처럼 @EnableSpringDataWebSupport 어노테이션을 사용하면 됨.

@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
public class WebAppConfig {
	...
}

설정 완료 시 도메인 클래스 컨버터와 페이징, 정렬을 위한 HandlerMethodArgumentResolver가 스프링 빈으로 등록됨. 등록되는 도메인 클래스 컨버터는 DomainClassConverter.

(2) 도메인 클래스 컨버터 기능

도메인 클래스 컨버터: HTTP 파라미터로 넘어온 엔티티의 아이디로 엔티티 객체를 찾아 바인딩 해줌.

예) 특정 회원 수정 화면을 보여줄 때, 컨트롤러는 HTTP 요청으로 넘어온 회원의 아이디를 사용해 레포지토리를 통해 회원 엔티티를 조회해야함.

적용 전

수정 화면 요청 URL: /member/memberUpdateForm?Id=1

  • 컨트롤러에서 파라미터로 넘어온 회원 아이디로 회원 엔티티를 찾음 → 찾은 회원 엔티티를 model을 사용해 뷰에 넘겨줌
@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 {

	@RequestMapping("member/memberUpdateForm")
	public String memberUpdateForm(@RequestParam("id") Member member, Model model){
		
		model.addAttribute("member", member);
		
		return "member/memberSaveForm";
	}
}

@RequestParam("id") Member member

  • HTTP 요청으로 회원 아이디를 받지만, 도메인 클래스 컨버터가 중간에 동작해 아이디를 회원 엔티티 객체로 변환해 넘겨줘 컨트롤러에서 단순하게 사용 가능
  • 도메인 클래스 컨버터는 해당 엔티티와 관련된 레포지토리를 사용해 엔티티를 찾음.

(3) 페이징과 정렬 기능

스프링 데이터가 제공하는 페이징과 정렬 기능을 스프링 MVC에서 편리하게 사용할 수 있도록 HandlerMethodArgumentResolver를 제공

  • 페이징 기능: 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은 다음 요청 파라미터 정보로 만들어짐.
  • 요청 파라미터:
    • page: 현재 페이지, 0부터 시작
    • size: 한 페이지에 노출할 데이터 건수
    • sort: 정렬 조건을 정의. 정렬 방향을 변경하려면 sort 파라미터를 추가하면됨. 예) ASC, DESC

접두사

사용해야할 페이징 정보가 둘 이상일 때 접두사를 사용해 구분. 접두사는 스프링 프레임워크가 제공하는 @Qualifier 어노테이션을 사용. “{접두사명}_”으로 구분.

public String list(
	@Qualifier("member") Pageable memberPageable,
	@Qualifier("order") Pageable orderPageable, ...
)

예: /members?member_page=0&order_page=1

기본값

Pageable의 기본값은 page=0, size-20. 기본값 변경을 위해서는 @PageableDefault 어노테이션을 사용

@RequestMapping(value="members_page", method=RequestMethod.GET)
public String list(@PageableDefault(size=12, sort="name", 
										direction=Sort.Direction.DESC) Pageable pageable) {
	...
}

12.8 스프링 데이터 JPA가 사용하는 구현체

스프링 데이터 JPA가 제공하는 공통 인터페이스는 SimpleJpaRepository 클래스가 구현

  • @Repository 적용: JPA 예외를 스프링이 추상화한 예외로 변환
  • @Transactional 트랜잭션 적용: JPA의 모든 변경은 트랜잭션 안에서 이루어져야함. 스프링 데이터 JPA가 제공하는 공통 인터페이스를 사용하면 데이터를 변경(등록/수정/삭제)하는 메소드에 @Transactional로 트랜잭션 처리가 되어있음.
  • @Transactional(readOnly=true): 데이터를 조회하는 메소드에 적용. 데이터 변경하지 않는 트랜잭션에서 적용해 플러시를 생략해 약간의 성능 향상을 얻을 수 있음.
  • save() 메소드: 저장할 엔티티가 새로운 엔티티이면 이미 저장, 아니면 병합. 필요 시 Persistable 인터페이스 구현해 판단 로직 변경 가능.

12.9 JPA 샵에 적용

기존의 웹 애플리케이션에 적용. 환경설정 → 레포지토리 리팩토링 → 명세 적용 → 기타.

(1) 환경설정

  • pom.xml에 spring-data-jpa 라이브러리를 추가.
<!-- 스프링 데이터 JPA -->
		<dependency>
			<groupId>org.springframework.data</groupId>
			<artifactId>spring-data-jpa</artifactId>
			<version>${spring-data-jpa.version}</version>
		</dependency>
  • appConfig에 <jpa:repositories>를 추가하고 base-package 속성에 레포지토리 위치를 지정
<jpa:repositories base-package="jpabook.jpashop.repository" />

(2) 레포지토리 리팩토링

회원 레포지토리

public interface MemberRepository extends JpaRepository<Member, Long> {

    List<Member> findByName(String name);
}

같은 방식으로 상품 등 다른 레포지토리 역시 리팩토링.

(3) 명세 적용

명세 검색 기능을 위해 레포지토리에 JpaSpecificationExecutor을 추가로 상속. 명세 작성을 위해 OrderSpec을 추가.

public class OrderSpec {

    public static Specification<Order> memberNameLike(final String memberName) {
        return new Specification<Order>() {
            public Predicate toPredicate(Root<Order> root, CriteriaQuery<?> query, CriteriaBuilder 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 new Specification<Order>() {
            public Predicate toPredicate(Root<Order> root, CriteriaQuery<?> query, CriteriaBuilder builder) {

                if (orderStatus == null) return null;

                return builder.equal(root.get("status"), orderStatus);
            }
        };
    }
}
  • OrderSearch 객체에 자신이 가진 검색조건으로 Specification을 생성하도록 코드 추가
public class OrderSearch {

    private String memberName;      //회원 이름
    private OrderStatus orderStatus;//주문 상태

    public String getMemberName() {
        return memberName;
    }

    public void setMemberName(String memberName) {
        this.memberName = memberName;
    }

    public OrderStatus getOrderStatus() {
        return orderStatus;
    }

    public void setOrderStatus(OrderStatus orderStatus) {
        this.orderStatus = orderStatus;
    }

    **public Specifications<Order> toSpecification() {
        return where(memberNameLike(memberName))
                .and(orderStatusEq(orderStatus));
    }**

}
  • 기존 레포지토리 검색 코드가 명세를 파라미터로 넘기도록 변경
public List<Order> findOrders(OrderSearch orderSearch) {
    	return orderRepository.findAll(orderSearch.toSpecification());
}

12.10 스프링 데이터 JPA와 QueryDSL 통합

스프링 데이터 JPA가 QueryDSL을 지원하는 방법은 2가지 존재.

  • QueryDslPredicateExecutor
  • QueryDslRepositorySupport

(1) QueryDslPredicateExecutor 사용

  • 레포지토리에서 QueryDslPredicateExecutor를 상속받으면 됨.
public interface ItemRepository extends JpaRepository<Item, Long>, QueryDslPredicateExecutor<Item> {}

QueryDSL 사용 예시

QItem item = QItem item;
Iterable<Item> result = itemRepository.findAll(
	item.name.contains("장난감").and(item.price.between(10000, 20000))
);

QueryDslPredicateExecutor 사용 예시

QueryDSL을 검색 조건으로 사용하며 스프링 데이터 JPA가 제공하는 페이징과 정렬 기능도 함께 사용 가능.

public interface QueryDslPredicateExecutor<T> {
	
	T findOne(Predicate predicate);
	Iterable<T> findAll(Predicate predicate);
	Iterable<T> findAll(Predicate predicate, OrderSpecifier<?>...orders);
	Page<T> findAll(Predicate predicate, Pageable pageable);
	long count(Predicate predicate);
}
  • 한계점: join, fetch를 사용할 수 없음.
  • 따라서 QueryDSL의 다양한 기능을 사용하기 위해서는 JPAQuery를 직접 사용하거나 스프링 데이터 JPA가 제공하는 QueryDslRepositorySupport를 사용해야함

(2) QueryDslRepositorySupport 사용

QueryDSL의 모든 기능을 사용하기 위해서는 JPAQuery 객체를 직접 생성해 사용하면 됨. 이때 스프링 데이터 JPA가 제공하는 QueryDslRepositorySupport를 상속받으면 더 편리하게 사용 가능함.

예제

  • CustomOrderRepository: 사용자 정의 레포지토리
    • 스프링 데이터 JPA가 제공하는 공통 인터페이스는 직접 구현할 수 없기에 사용자 정의 레포지토리를 만듦
public interface CustomOrderRepository {
	
	public List<Order> search(OrderSearch orderSearch);
}
  • QueryDslRepositorySupport 사용
public class OrderRepositoryImpl extends QueryDslRepositorySupport implements CustomOrderRepository {
	
	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의 기능

검색 조건에 따라 동적으로 쿼리를 생성. 생성자에서 QueryDslRepositorySupport에 엔티티 클래스 정보를 넘겨줘야함.

@Repository
public abstract class QueryDslRepositorySupport {
	
	//엔티티 매니저 반환
	protected EntityManager getEntityManager() {
		return entityManager;
	}
	
	//from절 반환
	protected JPQLQuery from(EntityPath<?> ... paths) {
		return querydsl.createQuery(paths);
	}
	
	//QueryDSL delete절 반환
	protected DeleteClause<JPADeleteClause> delete(EntityPath<?> path) {
		return new JPADeleteClause(entityManager, path);
	}
	
	//QueryDSL update절 반환
	protected UpdateClause<JPAUpdatedClause> update(EntityPath<?> path) {
		return new JPAUpdateClause(entityManager, path);
	}
	
	//스프링 데이터 JPA가 제공하는 Querydsl을 편하게 사용하도록 돕는 헬퍼 객체 반환
	protected QueryDsl getQueryDsl() {
		return this.querydsl;
	}
}
profile
이불 밖은 위험해.

0개의 댓글