본문 바로가기
개인공부/JPA

12장 스프링 데이터 JPA

by 응가1414 2024. 2. 8.

12장 스프링 데이터 JPA.pdf
1.67MB
12장 스프링 데이터 JPA.md
0.04MB
12장 스프링 데이터 JPA_2.md
0.02MB

12장. 스프링 데이터 JPA

12.1 스프링 데이터 JPA 소개

  1. 스프링 데이터 JPA는 스프링 프레임워크에서 JPA를 편리하게 사용할 수 있도록 지원하는 프로젝트다.
  2. 이 프로젝트는 데이터 접근 계층을 개발할 때 지루하게 반복되는 CRUD 문제를 세련된 방법으로 해결한다.
  3. 우선 CRUD를 처리하기 위한 공통 인터페이스를 제공한다.
  4. 리포지토리를 개발할 때 인터페이스만 작성하면 실행 시점에 스프링 데이터 JPA가 구현 객체를 동적으로 생성해서 주입해준다.
  5. 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> {}

12.1.1 스프링 데이터 프로젝트

  1. JPA는 스프링 데이터 프로젝트의 하위 프로젝트 중 하나다.
  2. 스프링 데이터(Spring Data) 프로젝트는 JPA, 몸고DB, ... 다양한 데이터 저장소에 대한 접근을 추상화해서 개발자의 편의를 제공

그림 12.2 스프링 데이터 연관 프로젝트

12.2 스프링 데이터 JPA 설정

  • JPA 환경 설정

예제 12.5 javaConfig 설정

@Configuration
@EnableJpaRepositories(basePackages = "org.example.repository")
public class AppConfig {
}
  • 스프링 데이터 JPA는 애플리케이션을 실행할 때 basePackage에 있는 리포지토리
  • 인터페이스들을 찾아서 해당 인터페이스를 구현한 클래스를 동적으로 생성한 다음 스프링 빈으로 등록한다.
  • 개발자가 직접 구현 클래스를 만들지 않아도 된다.

그림 12.3 구현 클래스 생성

12.3 공통 인터페이스 기능

  • 스프링 데이터 JPA는 간단한 CRUD 기능을 공통으로 처리하는 예제 12.6의 JpaRepository 인터페이스를 제공한다.
  • 스프링 데이터 JPA를 사용하는 가장 단순한 방법은
  • 예제 12.7과 같이 이 인터페이스를 상속받는 것이다.
  • 그리고 제네릭에 엔티티 클래스와 엔티티 클래스가 사용하는 식별자 타입을 지정하면 된다.

예제 12.6 JpaRepository 공통 기능 인터페이스

public interface JpaRepository<T, ID extends Serializable> extends PagingAndSortingRepository<T, ID>{}

예제 12.7 JpaRepository를 사용하는 인터페이스

public interface MemberRepository extends JpaRepository<Member, Long> {}
  • 예제 12.7의 상속받은 JpaRepository<Member, Long> 부분을 보면 제네릭에 회원 엔티티와 회원 엔티티의 식별자 타입을 지정했다.
  • 회원 리포지토리는 JpaRepository 인터페이스가 제공하는 다양한 기능을 사용할 수 있다.

  • 그림 12.4를 보면 윗부분에 스프링 데이터 모듈이 있고 그 안에 Repository, CrudRepository, PagingAndSortingRepositry가 있는데
  • 이것은 스프링 데이터 프로젝트가 공통으로 상요하는 인터페이스다.
  • 스프링 데이터 JPA가 제공하는 JpaRepository 인터페이스는 여기에 추가로 JPA에 특화된 기능을 제공한다.
  • JpaRepository 인터페이스를 상속받으면 사용할 수 있는 주요 메소드 몇 가지를 간단히 소개 하겠다.
  • 참고로 T는 엔티티, ID는 엔티티의 식별자 타입, S는 엔티티와 그 자식 타입을 뜻한다.
메서드 설명
Save(S) 새로운 엔티티는 저장하고 이미 있는 엔티티는 수정한다.
Delete (T) 엔티티 하나를 삭제한다. 내부에서 EntityManager.remove()를 호출한다.
findOne (ID) 엔티티 하나를 조회한다. 내부에서 EntityManager.find()를 호출한다.
getOne (ID) 엔티티를 프록시로 조회한다. 내부에서 EntityManager.getReference()를 호출한다.
findAll (...) 모든 엔티티를 조회한다. 정렬(sort)이나 페이징(Pageable) 조건을 파라미터로 제공할 수 있다.
  • save (S) 메소드는 엔티티에 식별자 값이 없으면 (null 이면) 새로운 엔티티로 판단해서 EntityManager.persist()를 호출하고
  • 식별자 값이 있으면 이미 있는 엔티티로 판단해서 EntityManager.merge()를 호출한다.
  • 필요하다면 스프링 데이터JPA의 기능을 확장해서 신규 엔티티 판단 전략을 변경할 수 있다.

12.4 쿼리 메소드 기능

  • 쿼리 메소드 기능은 스프링 데잍 JPA가 제공하는 마법 같은 기능이다.
  1. 메소드 이름으로 쿼리 생성
  2. 메소드 이름으로 JPA NamedQuery 호출
  3. @Query어노테이션을 사용해서 리포지토리 인터페이스에 쿼리 직접 정의

12.4.1 메소드 이름으로 쿼리 생성

  • 이메일과 이름으로 회원을 조회하려면 다음과 같은 메소드 정의
public interface MemberRepository extends Repository<Member, Long> {
    List<Member> findByEmailAndName(String email, String name);
}

JPQL 키워드와 Spring Data JPA 메서드

JPQL 키워드 Spring Data JPA 메서드 설명
And findByLastnameAndFirstname lastname = ?1firstname = ?2를 사용하여 검색
Or findByLastnameOrFirstname lastname = ?1 또는 firstname = ?2를 사용하여 검색
Is, Equals findByFirstname firstname = ?1을 사용하여 검색
Between findByStartDateBetween startDate?1?2 사이에 있는 항목 검색
LessThan findByAgeLessThan age < ?1인 항목 검색
LessThanEqual findByAgeLessThanEqual age <= ?1인 항목 검색
GreaterThan findByAgeGreaterThan age > ?1인 항목 검색
GreaterThanEqual findByAgeGreaterThanEqual age >= ?1인 항목 검색
After findByStartDateAfter startDate > ?1인 항목 검색
Before findByStartDateBefore startDate < ?1인 항목 검색
IsNull findByAgeIsNull agenull인 항목 검색
IsNotNull, NotNull findByAgeIsNotNull agenull이 아닌 항목 검색
Like findByFirstnameLike firstname?1을 포함하는 항목 검색
NotLike findByFirstnameNotLike firstname?1을 포함하지 않는 항목 검색
StartingWith findByFirstnameStartingWith firstname?1로 시작하는 항목 검색
EndingWith findByFirstnameEndingWith firstname?1로 끝나는 항목 검색
Containing findByFirstnameContaining firstname?1을 포함하는 항목 검색
OrderBy findByAgeOrderByLastnameDesc age = ?1로 필터링한 결과를 lastname 내림차순으로 정렬
Not findByLastnameNot lastname?1이 아닌 항목 검색
In findByAgeIn(Collection ages) age가 주어진 컬렉션 ages 내에 있는 항목 검색
NotIn findByAgeNotIn(Collection age) age가 주어진 컬렉션 age 내에 없는 항목 검색
JPQL 메서드 JPQL 쿼리 설명
TRUE findByActiveTrue() WHERE x.active = true activetrue인 엔터티를 검색하는 메서드
FALSE findByActiveFalse() WHERE x.active = false activefalse인 엔터티를 검색하는 메서드
IgnoreCase findByFirstnameIgnoreCase WHERE UPPER(x.firstname) = UPPER(?1) 대소문자를 무시하고 firstname과 주어진 값과 일치하는 엔터티를 검색하는 메서드

12.4.2 JPA NamedQuery

  • 스프링 데이터 JPA는 메소드 이름으로 JPA Named 퀄리를 호출하는 기능을 한다.
  • JPA Named 쿼리는 이름 그대로 쿼리에 이름을 부여해서 상요하는 방법인데
  • 예지 12.8과 같이 어노테이션이나 예제 12.9와 같이 XML에 쿼리를 정의 할수 있다.
  • 상세내용은 10.2.15절 참고

예제 12.8 @NamedQuery 어노테이션으로 Named 쿼리 정의

@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>

예제 12.10 JPA를 직겆ㅂ 사용해서 Named 쿼리 호출

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

예제 12.11 스프링 데이터 JPA로 Named 쿼리 호출

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

12.4.3 @Query, 리포지토리 메소드에 쿼리 정의

  • 리포지토리 메소드에 직접 쿼리를 정의 하려면
  • 예제 12.12와 같이 @org.springframework.data.jpa.repository.query 어노테이션 사용
  • JPA Named 쿼리처럼 애플리케이션 실행 시점에 문법 오류를 발견할 수 있는 장점이 있다.

예제 12.12 메소드에 JPQL 쿼리 작성

public interface MemberRepository extends JpaRepository<Member, Long> {
    @Query("select m from Member m where m.username = ?1")
    Member findByUsername(String username);
}

예제 12.13와 같이 @Query 어노테이션에 nativeQuery = true를 설정한다.

참고로 스프링 데이터 JPA가 지원하는 파라미터 바인딩을 사용하면 JPQL은 위치 기반 파라미터를 1부터 시작하지만

네이티브 SQL은 예제 12.13과 같이 0부터 시작한다.

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

12.4.4 파라미터 바인딩

  • 스프링 데이터 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")
    // 이름 기반 파라미터 바인딩은 @Param을 사용한다. 
  Member findByUsername(@Param("name") String username);
}

12.4.5 벌크성 수정 쿼리

예제 12.15를 통해 JPA로 작성한 벌크성 수정 쿼리부터 보자.

예제 12.16 스프링 데이터 JPA를 사용한 벌크성 수정 쿼리

@Modifying
@Query("update Product p set p.price = p.price * 1.1 where p.stockAmount < : stockAmount")
int bulkPriceUp(@Param("stockAmount") String stockAmount);
  • 스프링 데이터 JPA에서 벌크성 수정, 삭제 쿼리는 org. Springframework.data.jpa.repository.Modifying어노테이션을 사용하면 된다.
  • 벌크성 쿼리를 실행하고 나서 영속성 컨텍스트를 초기화 하고 싶으면
  • @Modifying(clearAutomatically = true)
  • 처럼 clearAutomatically 옵션을 ture로 설정하면 된다. 참고로 이 옵션의 기본값은 false다.

12.4.6 반환 타입

  • 스프링 데이터 JPA는 유연한 반환 타입을 지원하는데 결과가 한 건 이상이면 컬렉션 인터페이스를 사용하고
  • 단건이면 반환 타입을 지정한다.
List<Member> findByName(String name); // 컬렉션
Member findByEmail(String email); // 단건 
  • 만약 조회 결과가 없으면 컬렉션은 빈 컬렉션을 반환하고 단건은 null을 반환한다.
  • 그리고 단건을 기대하고 반환 타입을 저정했는데 결과가 2건 이상 조회되면 javax.persistence.NonUniqueResultExceptin예외가 발생한다.
  • 참고로 단건으로 지정한 메소드를 호출하면 스프링 데이터 JPA는 내부에서 JPQL의 Query.getSingleResult() 메소드를 호출한다.
    • 조회 결과가 없으면 javax.persistence.NoResultException 예외가 발생하는데 개발자 입장에서 다루기가 상당히 불편하다.
    • 스프링 데이터 JPA는 단건을 조회할 때 이 예외가 발생하면 예외를 무시하고 대신에 null을 반환한다.

12.4.7 페이징과 정렬

  • 스프링 데이터 JPA는 쿼리 메소드에 페이징과 정렬 기능을 사용할 수 있도록 2가지 특별한 파라미터를 제공한다.
  • org.springframework.data.domain.Sort : 정렬 기능
  • org.springframework.data.domain.Pageable: 페이징 기능 (내부에 Sort 포함)

예제 12.17과 같이 파라미터에 Pageable을 사용하면 반환 타입으로 List나 org.springframework.data.domain.Page를 사용할 수 있다.

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

예제 12.17 페이징과 정렬 사용 예제

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

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

List<Member> findByName(String name, Sort sort);
  • 다음 조건으로 페이징과 정렬을 사용하는 예제 코드를 보자.
  • 검색 조건: 이름이 김으로 시작하는 회원
  • 정렬 조건: 이름으로 내림차순
  • 페이징 조건: 첫 번째 페이지, 페이지당 보여줄 데이터는 10건

예제 12.18 Page 사용 예제 정의 코드

public interface MemberRepository extends Repository<Member, Long>{
  Page<Member> findByNameStartingWith(String name, Pageable Pageable);
}

예제 12.19 Page tㅏ용 예제 실행 코드

// 페이징 조건과 정렬 조건 설정
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(); // 다음 페이지 존재 여부

두 번째 파라미터로 받은 Pagable은 인터페이스다.

따라서 실제 사용할 때는 예제 12.19와 같이 해당 인터페이스를 구현함 org.springframework.data.domain.PageRequest 객체를 사용한다.

PageRequest 생성자의 첫 번째 파라미터에는 현재 페이지를,

두 번째 파라미터에는 조회할 데이터 수를 입력한다. 여기에 추가로 정렬 정보도 파라미터로 사용할 수 있다. 참고로 페이지는 0부터 시작한다.

예제 12.20을 통해 반환 타입인 Page 인터페이스가 제공하는 다양한 메소드를 보자.

예제 12.20 Page 인터페이스

  1. 페이지 정보 관련 메서드:
    • getNumber(): int (현재 페이지 번호)
    • getSize(): int (페이지 크기)
    • getTotalPages(): int (전체 페이지 수)
    • getNumberOfElements(): int (현재 페이지에 포함된 요소 수)
    • getTotalElements(): long (전체 요소 수)
  2. 페이지 위치 관련 메서드:
    • hasPreviousPage(): boolean (이전 페이지 존재 여부)
    • isFirstPage(): boolean (현재 페이지가 첫 페이지인지 여부)
    • hasNextPage(): boolean (다음 페이지 존재 여부)
    • isLastPage(): boolean (현재 페이지가 마지막 페이지인지 여부)
  3. 페이지 이동 관련 메서드:
    • nextPageable(): Pageable 또는 null (다음 페이지의 페이지 설정 정보)
    • previousPageable(): Pageable 또는 null (이전 페이지의 페이지 설정 정보)
  4. 조회된 데이터 관련 메서드:
    • getContent(): List (현재 페이지에 포함된 요소 리스트)
    • hasContent(): boolean (현재 페이지에 내용이 있는지 여부)
  5. 정렬 정보 관련 메서드:
    • getSort(): Sort (페이지 요소의 정렬 정보)

Pageable과 Page를 사용하면 지루하고 반복적인 페이징 처리를 손쉽게 개발할 수 있다.

12.4.8 힌트

  • JPA 쿼리 힌트를 사용하려면 org.springframework.data.jpa.repository.QueryHints 어노테이션을 사용하면 된다.
  • 참고로 이것은 SQL 힌트가 아니라 JPA 구현체에게 제공하는 힌트이다.
@QueryHints(value = 
           @QueryHint (name = "org.hibernate.readOnly",
                     value = "true",
                     forCounting = true )
           )
  • forCounting 속성은 반환 타입으로 Page 인터페이스를 적용하면 추가로 호출하는 페이징을 위한 count 쿼리에도 쿼리 힌트를 적용할지를 설정하는 옵션이다. (기본값 true)

12.4.9 LOCK

  • 쿼리 시 락을 걸려면 org.springframework.data.jpa.repository.Lock어노테이션을 사용하면 된다.
  • JPA가 제공하는 락에 대해서는 16.1절을 참고하자
@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findByName(String name);

12.5 명세

  • 책 도메인 주도 설계(Domain Driven Design)는 명세(SPECIFICATION)라는 개념을 소개하는데
  • 스프링 데이터 JPA는 JPA Criteria로 이개념을 사용할 수 있도록 지원한다.
  • 명세(Specification)를 이해하기 위한 핵심 단어는 술어(predicate)인데 이것은 단순히 참이나 거짓으로 평가된다.
  • 그리고 이것은 AND, OR같은 연산자로 조합할 수 있다.
  • 예를 들어 데이터를 검색하기 위한 제약 조건 하나하나를 술어라 할 수 있다.
  • 이 술어를 스프링 데이터 JPA는 org.springframework.data.jpa.domain.Specification 클래스로 정의 해싿.

Specification은 컴포지트 패턴(en.wikipedia.org/wiki/Composite_pattern)으로 구성되어 있어서 여러 Specification을 조합할 수 있다.

따라서 다양한 검색조건을 조립해서 새로운 검색 조건을 쉽게 만들 수 있다.

따라서 다양한 검색조건을 조립해서 새로운 검색조건을 쉽게 만들 수 있다.

명세 기능을 사용하려면 예제 12.21과 같이 리포지토리에서 org.springframework.data.jpa.repository.JpaSpecificationExecutor 인터페이스를 상속받으면 된다.

예제 12.21 JpaSpecificationExecutor 상속

public interface OrderRepository extends JpaRepository<Order, Long>, 
            JpaSpecificationExecutor<Order>{...}

예제 12.22 JpaSpecificationExecutor 인터페이스

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(Specification<T> spec);
}
  • 예제 12.22의 JpaSpecificationExecutor의 메소드들은 Specification을 파라미터로 받아서 검색 조건으로 사용한다.
  • 이제 명세를 사용하는 예제 12.23을 보자. 우선 명세를 사용하는 코드를 보고나서 명세를 정의하는 코드를 보겠다.

예제 12.23 명세 사용코드

import static org.springframework.data.jpa.domain.Specification.*;

import static jpabook.jpashop.domain.spec.OrderSpec.*;

public List<Order> findOrders(String name) {
  List<Order> result = orderRepository.findAll(
        where(memberName(name)).and(isOrderStatus())
  );

  return result;
}
  • Specification는 명세들을 조립할 수 있도록 도와주는 클래스인데 where(), and(), or(), not() 메소드를 제공한다.
  • findAll을 보면 회원 이름 명세(memberName)와 주문 상태 명세(isOrderStatus)를 and로 조합해서 검색 조건으로 사용한다.
  • 이제 예제 12.24의 OrderSpec 명세를 정의하는 코드를 보자.

예제 12.24 OrderSpec 명세 정의 코드

package jpabook.jpashop.domain;

import org.springframework.data.jpa.domain.Specification;
import org.springframework.util.StringUtils;import javax.persistence.criteria.*; 
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> isOrderStatus(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);            
      }        
    };    
    }
}
  • 명세를 정의하려면 Specification인터페이스를 구현하면 된다.
  • 예제에서는 편의상 내부 무명 클래스를 사용했다.
  • 명세를 정의 할 때는 toPredicate(...) 메소드만 구현하면 되는데
  • JPA Critera의 Root, CriteriaQuery, CriteriaBuilder클래스가 모두 파라미터로 주어진다.
  • 이 파라미터들을 활용해서 적절한 검색 조건을 반환하면 된다.
  • JPA Criteria에 대한 이해가 부족하다면 10.3절을 참고하자.

12.6 사용자 정의 리포지토리 구현 // 리포지토리 커스텀

  • 스프링 데이터 JPA로 리포지토리를 계발하면 인터페이스만 정의하고 구현체는 만들지 않는다.
  • 하지만 다양한 이유로 메소드를 직접 구현해야 할 때도 있다.
  • 그렇다고 리포지토리를 직접 구현하면 공통 인터페이스가 제공하는 기능까지 모두 구현해야 한다.
  • 스프링 데이터 JPA는 이런 문제를 우회해서 필요한 메소드만 구현할 수 있는 방법을 제공한다.
  • 먼저 직접 구현할 메소드를 위한 예제 12.25와 같은 사용자 정의 인터페이스를 작성해야 한다.
  • 이때 인터페이스 이름은 자유롭게 지으면 된다.

예제 12.25 사용자 정의 인터페이스

public interface MemberRepositoryCustom {
    public List<Member> findMemberCustom();
}
  • 다음으로 예제 12.26과 같은 사용자 정의 인터페이스를 구현한 클래스를 작성해야 한다.
  • 이때 클래스 이름을 짓는 규칙이 있는데 리포지토리 인터페이스 이름 + Impl로 지어야 한다.
  • 이렇게 하면 스프링 데이터 JPA가 사용자 정의 구현 클래스로 인식한다.

예제 12.26 사용자 정의 구현 클래스

public class MemberRepositoryImpl implements MemberRepositoryCustom {
  @Override
  public List<Member> findMemberCustom(){
    ... // 사용자 정의 구현
  }
}
  • 마지막으로 예제 12.27과 같이 리포지토리 인터페이스에서 사용자 정의 인터페이스를 상속받으면 된다.

예제 12.27 사용자 정의 인터페이스 상속

public interface MemberRepository 
            extends JpaRepository<Member, Long>, MemberRepositoryCustom{...}
  • 만약 사용자 정의 구현 클래스 이름 끝에 Impl 대신 다른 이름을 붙이고 싶으면 repository-impl-postfix 속성을 변경하면 된다. 참고로 Impl이 기본값이다.

Xml 설정

<repositories base-package="jpabook.jpashop.repository" repository-impl-postfix="impl" />

JavaConfig 설정

@EnableJpaRepositories(basePackages = "jpabook.jpashop.repository", repositoryImplementationPostfix = "impl")

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

예제 12.31 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 (entityInformaion.isNew(entity)) {
            em.persist(entity);
            return entity;
        } else {
            return em.merge(entity);
        }
    }
    // 나머지 코드 생략
}
  • @Repository 적용: JPA 예외를 스프링이 추상화한 예외로 변환한다.
  • @Transactional 트랜잭션 적용: JPA의 모든 변경은 트랜잭션 안에서 이루어져야 한다. 스프링 데이터 JPA가 제공하는 공통 인터페이스를 사용하면 데이터를 변경(등록, 수정, 삭제)하는 메소드에 @Transactional로 트랜잭션 처리가 되어 있 다. 따라서 서비스 계층에서 트랜잭션을 시작하지 않으면 리포지토리에서 트랜잭션을 시작한다. 물론 서비스 계층에서 트랜잭션을 시작했으면 리포지토리도 해당 트랜잭션을 전파받아서 그대로 사용한다.
  • @Transactional (readOnly = true): 데이터를 조회하는 메소드에는 readOnly = true 옵션이 적용되어 있다. 데이터를 변경하지 않는 트랜잭션에서 readOnly = true 옵션을 사용하면 플러시를 생략해서 약간의 성능 향상을 얻을 수 있는 데, 자세한 내용은 15.4.2절에서 설명하겠다.
  • save() 메소드: 이 메소드는 저장할 엔티티가 새로운 엔티티면 저장(persist)하고 이미 있는 엔티티면 병합(merge)한다. 새로운 엔티티를 판단하는 기본 전략은 엔티티의 식별자로 판단하는데 식별자가 객체일 때 null, 자바 기본 타입일 때 숫자 0 값이면 새로운 엔티티로 판단한다.

필요하면 엔티티에 예제 12.32의 Persistable 인터페이스를 구현해서 판단 로직을 변경할 수 있다.

예제 12.32 Persistable

public interface Persistable<ID extends Serializable> extends Serializable {
    ID getId();
    boolean isNew();
}

12.9 JPA 샵에 적용

12.9.3 명세 적용

  • 명세(Specification)로 검색하는 기능을 사용하려면 리포지토리에 org.springframework.data.jpa.repository.JpaSpecificationExecutor을 추가로 상속

예제 12.41 OrderSpec 추가

package jpabook.jpashop.domain;

import org.springframework.data.jpa.domain.Specification;
import org.springframework.util.StringUtils;

import javax.persistence.criteria.*;

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);
            }
        };
    }
}
  • 검색조건을 가지고 있는 예제 12.42의 OrderSearch 객체에 자신이 가진 검색 조건으로 Specification을 생성하도록 코드를 추가하자.

예제 12.42 검색 객체가 Specification 생성하도록 추가

package jpabook.jpashop.domain;

import org.springframework.data.jpa.domain.Specifications;
import static jpabook.jpashop.domain.OrderSpec.memberNameLike;
import static jpabook.jpashop.domain.OrderSpec.orderStatusEq;
import static org.springframework.data.jpa.domain.Specifications.where;

public class OrderSearch {
    private String memberName; // 회원명 sibet HeC OneL
    private OrderStatus orderStatus; // 주문 상태

    // Getter, Setter

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

예제 12.43, 예제 12.44 리팩토링 전, 후

// 예제 12.43 OrderService, findOrders 리팩토링 전
public List<Order> findOrders(OrderSearch orderSearch) {
    return orderRepository.findAll(orderSearch);
}

// 예제 12.44 OrderService, findOrders 리팩토링 후
public List<Order> findOrders(OrderSearch orderSearch) {
    return orderRepository.findAll(orderSearch.toSpecification());
}

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

  • 스프링 데이터 JPA에 추가로 QueryDSL을 사용해보자.

■ org.springframework.data.querydsl.QueryDslPredicateExecutor

■ org.springframework.data.querydsl.QueryDslRepositorySupport

12.10.1 QueryDslPredicateExecutor 사용

  • 첫 번째 방법은 다음처럼 리포지토리에서 QueryDslPredicateExecutor를 상속받 으면 된다.
public interface ItemRepository 
extends JpaRepository<Item, Long>, QueryDslPredicateExecutor<Item> { 
} 
  • 이제 상품 리포지토리에서 QueryDSI 울 사용할 수 있다.
  • 예제 12.45는 QueryDSL이 생성한 쿼리 타입으로 장난감이라는 이름을 포함하 고 있으면서 가격이 10000~20000원인 상품을 검색한다.

예제 12.45 QueryDSL 사용 예제

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

예제 12.46의 QueryDslPredicateExecutor 인터페이스를 보면 QueryDSL을 검색조건으로 사용하면서 스프링 데이터 JPA가 제공하는 페이징과 정렬 기능도 함 께 사용할 수 있다.

예제 12.46 QueryDslPredicateExecutor 인터페이스

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

예제 12.47 CustomOrderRepository 사용자 정의 리포지토리

// 예제 12.47 CustomOrderRepository 사용자 정의 리포지토리
package jpabook.jpashop.repository.custom;

import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderSearch;

import java.util.List;

public interface CustomOrderRepository {
    List<Order> search(OrderSearch orderSearch);
}

12.10.2 QueryDsIRepositorySupport 사용

QueryDSL의 모든 기능을 사용하려면 JPAQuery 객체를 직접 생성해서 사용하면 된다. 이때 스프링 데이터 JPA가 제공하는 QueryDsIRepositorySupport를 상속 받아 사용하면 조금 더 편리하게 QueryDSL을 사용할 수 있다.

예제 12.47을 통해 사용 예제를 보자.

예제 12.47 CustomOrderRepository 사용자 정의 리포지토리

package jpabook. jpashop. repository. custom; 

import jpabook. jpashop. domain.Order; 
import jpabook. jpashop.domain. OrderSearch; 
import java. util.List; 

public interface CustomOrderRepository { 
    public List<Order> search(OrderSearch orderSearch);
}
  • 스프링 데이터 JPA가 제공하는 공통 인터페이스는 직접 구현할 수 없기 때문에
  • 예제 12.47에서 customOrderRepository라는 사용자 정의 리포지토리를 만들었다.
  • 이제 예제 12.16을 통해 QueryDslRepositorySupport를 사용하는 코드를 보자.

예제 12.48 QueryDslRepositorySupport 사용 코드

// 예제 12.48 QueryDslRepositorySupport 사용 코드
package jpabook.jpashop.repository.custom;

import com.mysema.query.jpa.JPQLQuery;
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.domain.OrderSearch;
import jpabook.jpashop.domain.QOrder;
import jpabook.jpashop.domain.QMember;
import org.springframework.data.jpa.repository.support.QueryDslRepositorySupport;
import org.springframework.util.StringUtils;

import java.util.List;

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);
    }
}
  • 예제 12.48은 웹 애플리케이션 만들기에서 사용했던 주문 내역 검색 기 능을 QueryDsIRepositorysupport를 사용해서
  • QueryDSL로 구현한 예제다.
  • 검색 조건에 따라 동적으로 퀴리를 생성한다.
  • 참고로 생성자에서 QueryDsIRepositorysupport에 엔티티 클래스 정보를 넘겨주어야 한다.
  • 예제 12.49는 QueryDsIRepositorysupport의 핵심 기능만 간추러보았다.
package org.springframework.data.jpa.repository.support;

import org.springframework.stereotype.Repository;

import javax.persistence.EntityManager;

@Repository
public abstract class QueryDslRepositorySupport {

    protected EntityManager entityManager;
    protected Querydsl querydsl;

      // 엔티티 매니저 반환
    protected QueryDslRepositorySupport(EntityManager entityManager) {
        this.entityManager = entityManager;
        this.querydsl = new Querydsl(entityManager, new PathBuilderFactory().create(getClass()));
    }

      // 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<JPAUpdateClause> update(EntityPath<?> path) {
        return new JPAUpdateClause(entityManager, path);
    }

      // 스프링 데이터 JPA가 제공하는 Querydsl을 편하게 사용하도록 돕느느
      // 핼퍼 객체 반환
    protected Querydsl getQuerydsl() {
        return this.querydsl;
    }
}

'개인공부 > JPA' 카테고리의 다른 글

10장 객체지향 쿼리 언어  (1) 2024.02.05
9장 값 타입  (2) 2024.02.05
8장 프록시와 연관관계 관리  (0) 2024.02.05
7장 고급 매핑  (2) 2024.02.05
6장 다양한 연관관계 매핑  (1) 2024.02.05