12장 스프링 데이터 JPA

이주호·2025년 1월 19일

스프링 데이터 JPA 소개

스프링 데이터 JPA는 스프링 프레임워크에서 JPA를 편리하게 사용할 수 있도록 지원하는 프로젝트다.
반복적인 CRUD 작업을 할 때 해결하기 위해 스프링 프레임 워크에서는 스프링 데이터 JPA를 이용할 수 있다. 공통 CRUD 인터페이스를 제공하고 리포지토리를 개발할 때 인터페이스만 작성하면 실행 시점에 스프링 데이터 JPA가 구현 객체를 동적으로 생성해서 주입해준다. 따라서 개발자는 데이터 접근 계층을 개발할 때 구현 클래스 없이 인터페이스만 작성해도 개발을 완료할 수 있다.
CRUD를 처리하기 위한 공통 메서드는 스프링 데이터 JPA가 제공하는 org.springframework.data.jpa.repository.JpaRepository 인터페이스에 있다.

예제

public interface MemberRepository extends JpaRepository<Member, Long> {
	Member findByUserName(String username);
}

위처럼 공통적으로 처리할 수 없는 MemberRepository.findByUserName(...)은 스프링 데이터 JPA에서 메소드 이름을 분석해서 다음 JPQL을 실행한다.

select m from Member m where username =:username

공통 인터페이스 기능

스프링 데이터 JPA의 공통 인터페이스를 사용하는 가장 간단한 방법은 위에서 보여준 예제처럼 JpaRepository 인터페이스를 상속받는 것이다. 제네릭 엔티티 클래스와 엔티티 클래스가 사용하는 식별자 타입을 지정하여 사용한다.

public interface MemberRepository extends JpaRepository<Member, Long> {
	Member findByUserName(String username);
}

위처럼 JpaRepository 인터페이스를 상속받았을 때 다양한 기능을 사용할 수 있는데, 이를 알아보기 위해 JpaRepository 인터페이스의 계층 구조를 살펴보자.

스프링 데이터 모듈안에 공통으로 사용하는 인터페이스가 있고 스프링 데이터 JPA가 제공하는 JpaRepository 인터페이스는 여기에 추가로 JPA에 특화된 기능을 제공한다.
참고로 T는 엔티티, ID는 엔티티의 식별자 타입, S는 엔티티와 그 자식 타입을 의미한다.
몇 가지 주요 메서드를 살펴보자.

  • save(S) : 엔티티의 식별자 값이 없으면(null) 새로운 엔티티로 판단해서 EntityManager.persist()를 호출하고 식별자 값이 있으면 이미 있는 엔티티로 판단해서 EntityManager.merge()를 호출한다. 필요하다면 스프링 데이터 JPA 기능을 확장해서 신규 엔티티 판단 전략을 변경할 수 있다.
  • delete(T) : 엔티티 하나를 삭제한다. 내부에서 EntityManager.remove()를 호출한다.
  • findOne(ID) : 엔티티 하나를 조회한다. 내부에서 EntityManager.find()를 호출한다.
  • getOne(ID) : 엔티티를 프록시로 조회한다. 내부에서 EntityManager.getReference()를 호출한다.
  • findAll(...) : 모든 엔티티를 조회한다. 정렬(Sort)이나 페이징(Pageable) 조건을 파라미터로 제공할 수 있다.

쿼리 메소드 기능

스프링 데이터 JPA의 쿼리 메소드 기능은 Repository 인터페이스의 메소드 이름을 기반으로 데이터베이스 쿼리를 자동 생성하는 기능이다. 이 기능을 통해 개발자는 복잡한 JPQL 또는 SQL을 작성하지 않고도 데이터베이스와 상호작용하는 쿼리를 생성할 수 있다.

메소드 이름으로 쿼리 생성

findBy, countBy와 같이 스프링 데이터 JPA 공식 문서가 제공하는 규칙에 따라서 메소드를 정의하면 해당 메서드를 실행할 때 메소드 이름을 분석해서 JPQL을 생성하고 실행한다.

아래는 이메일과 이름으로 회원을 조회하는 예시이다.

public interface MemberRepository extends JpaRepository<Member, Long> {
	List<Member> findByEmailAndName(String email, String name);
}
//실행 JPQL
select m from Member m where m.email = ?1 and m.name = ?2

스프링 데이터 JPA 공식 문서가 제공하는 쿼리 생성 기능은 아래와 같다.

더 자세한 쿼리 생성 기능은 아래 공식 문서를 참고하자.
https://docs.spring.io/spring-data/jpa/reference/jpa/query-methods.html

JPA NamedQuery

스프링 데이터 JPA는 메소드 이름으로 JPA Naemd 쿼리를 호출하는 기능을 제공한다. JPA Named 쿼리는 쿼리에 이름을 부여해서 사용하는 방법인데, @NamedQuery 어노테이션이나 XML에 쿼리를 정의할 수 있다.
(자세한 내용은 10장에서 설명했으므로 넘어가겠다.)

public interface MemberRepository 
		extends JpaRepository<Member, Long> {  // NamedQuery 선언한 Member 도메인 클래스
	List<Member> findByUserame(@Param("username") String username);
}

스프링 데이터 JPA는 선언한 "도메인 클래스 + .(점) + 메서드 이름"으로 Named 쿼리를 찾아서 실행한다. 만약 실행할 Named 쿼리가 없으면 메서드 이름으로 쿼리 생성 전략을 사용한다.

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

리포지토리 메소드에 직접 쿼리를 정의하려면 @org.springframework.data.jpa.repository.Query 어노테이션을 사용할 수 있다. 실행할 메서드에 정적 쿼리를 직접 작성하므로 이름 없는 Named 쿼리라 할 수 있다.JPA Named 쿼리처럼 애플리케이션 실행 시점에 문법 오류를 발견할 수 있다는 장점이 있다.

네이티브 SQL을 사용하려면 @Query 어노테이션에 nativeQuery = true 로 설정해서 사용할 수 있다.
이 때, JPQL은 위치 기반 파라미터를 1부터 시작하지만 네이티브 SQL은 0부터 시작한다는 차이점이 있다.


// 메서드에 JPQL 쿼리 작성
public interface MemberRepository extends JpaRepository<Member, Long> {
	@Query("select m from Member m where m.username = ?1")
	List<Member> findByUsername(String username);
}

// JPA 네이티브 SQL 지원
public interface MemberRepository extends JpaRepository<Member, Long> {
	@Query(value = "SELECT * FROM MEMBER WHERE USERNAME = ?0",
    		nativeQuery = true)
	List<Member> findByUsername(String username);
}

파라미터 파인딩

스프링 데이터 JPA는 위치 기반 파라미터 바인딩과 이름 기반 파라미터 바인딩을 모두 지원한다. 코드 가독성과 유지보수를 위해 이름 기반 파라미터 바인딩을 사용하는 것을 추천한다.

이름 기반 파라미터 바인딩을 사용하려면 org.springframework.data.repository.query.Param(파라미터 이름)어노테이션을 사용하면 된다.

import org.springframework.data.repository.query.Param

public interface MemberRepository extends JpaRepository<Member, Long> {
	@Query("select m from Member m where m.username =:name")
	List<Member> findByUsername(@Param("naem")String username);
}

벌크성 수정 쿼리

스프링 데이터 JPA에서 벌크성 수정, 삭제 쿼리는 org.springframework.data.jpa.repository.Modifying 어노테이션을 사용하면 된다. 벌크성 쿼리를 실행하고 나서 영속성 컨텍스트를 초기화하고 싶으면 @Modifying(clearAutomatically = true) 처럼 해당 속성을 true로 설정하면 된다.

@Modifying
@Query("update Product p set p.price = p.price * 1.1
			where p.stockAmount < :stockAmount")
int bulkPriceUp(@Param("stockAmount") String stockAmount);
// ProductRepository 예제
public interface ProductRepository extends JpaRepository<Product, Long> {
    @Modifying(clearAutomatically = true)
    @Query("update Product p set p.price = p.price * 1.1 where p.stockAmount < :stockAmount")
    int bulkPriceUp(@Param("stockAmount") String stockAmount);
}

반환 타입

스프링 데이터 JPA는 유연한 반환타입을 지원하는데 결과가 한 건 이상이면 컬렉션을 반환하고 단건이면 반환 타입을 지정한다.
이 때, 조회 결과가 없으면 컬렉션은 빈 컬렉션을 반환하고, 단건은 null을 반환한다. 단건 조회에서 2건 이상이 조회되면 javax.persistence.NonUniqueResultException 예외가 발생한다.

페이징과 정렬

스프링 데이터 JPA는 쿼리 메서드에 페이징과 정렬 기능을 사용할 수 있게 2가지 파라미터를 제공한다.

  • org.springframework.data.domain.Sort: 정렬기능
  • org.springframework.data.domain.Pageable: 페이징 기능(내부에 Sort 포함)

파라미터에 Pageable을 사용하면 반환 타입으로 List나 org.springframework.data.domain.Page를 사용할 수 있다. 이 때, Page를 반환타입으로 지정하면 페이징 기능을 제공하기 위해 검색된 전체 데이터 건수를 조회하는 count 쿼리를 추가로 호출한다.

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

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

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

페이징 예제

검색 조건 : 이름이 김으로 시작하는 회원
정렬 조건 : 이름으로 내림차순
페이징 조건 : 첫 번째 페이지, 페이지당 보여줄 데이터는 10건

public interface MemberRepository extends JpaRepository<Member, Long> {
	Page<Member> findByNameStartingWith(String name, Pageble pageble);
}

//페이징 조건과 정렬 조건 설정
PageRequest pageRequest =
	new PageRequest(0, 10, new Sort(Direction.DESC, "name"));
    
Page<Member> result = 
	memberRepository.findByNameStartingWith("김", pageRequest);
    
List<Member> members = result.getContent(); //조회된 데이터    

리포지토리에서 정의한 메서드에 두 번째 파라미터 Pageable은 인터페이스다. 따라서 실제 사용할 때는 해당 인터페이스를 구현한 org.springframework.data.domain.PageRequest 객체를 사용한다. PageRequest 생성자의 첫 번째 파라미터에는 현재 페이지를, 두 번째 파라미터에는 조회할 데이터 수를 입력한다. 여기에 추가로 정렬정보도 파라미터로 사용할 수 있다. 참고로 페이지는 0부터 시작한다.

힌트

JPA에서 쿼리 힌트(Query Hint)는 특정 쿼리에 대해 JPA 구현체가 사용할 힌트를 제공하는 기능입니다. 주로 성능 최적화나 쿼리 동작 변경을 위해 사용됩니다. 힌트는 JPA 표준이 제공하는 기본 기능이며, 구현체에 따라 지원하는 힌트가 다를 수 있습니다.

사용 목적
읽기 전용 쿼리 최적화
예: 쿼리 결과를 수정하지 않을 경우, 읽기 전용 힌트를 추가하여 내부적으로 성능 최적화를 할 수 있습니다.
캐시 사용 설정
쿼리 실행 시, 2차 캐시를 사용할지 여부를 설정할 수 있습니다.
JPA 구현체 전용 기능 활용
특정 구현체(예: Hibernate)에서 제공하는 고유한 힌트를 활용할 수 있습니다.

JPA 쿼리 힌트를 사용하려면 org.springframework.data.jpa.repository.QueryHints 어노테이션을 사용하면 된다. 이것은 SQL 힌트가 아니라 JPA 구현체에게 제공하는 힌트다.

@QueryHints(value = {@QueryHint(name = "org.hibernate.readOnly", value = "true")},
						forCounting = true)
Page<Member> findByNameStartingWith(String name, Pageble pageble);

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

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

스프링 데이터 JPA는 인터페이만 정의하고 구현체는 개발하지 않는다. 하지만 다양한 이유로 직접 메서드를 구현해야 할 때가 있는데 이 때 리포지토리를 직접 구현하면 공통 인터페이스가 제공하는 기능까지 모두 구현해야 한다. 이를 피하기 위해서 필요한 메서드만 구현할 수 있는 방법이 있다.

먼저 직접 구현할 메서드를 위해 사용자 정의 인터페이스를 작성해야 한다. (이름은 자유)

// 사용자 정의 인터페이스
public interface MemberRepositoryCustom {
	public List<Member> findMemberCustom();
}    

다음으로 사용자 정의 인터페이스 구현체를 만드는데, 구현 클래스의 이름 규칙이 존재한다. 리포지토리 인터페이스 이름 + Impl로 작성해야 한다. 이렇게 해야 스프링 데이터 JPA가 사용자 정의 구현 클래스로 인식한다.

public class MemberRepositoryImpl implements MemberRepositoryCustom {

	@Override
    public List<Member> findMemberCuston() {
    	...//사용자 정의 구현
    }
}    

마지막으로 리포지토리 인터페이스에서 사용자 정의 인터페이스를 상속받으면 된다.

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

스프링 데이터 JPA와 QueryDSL 통합

스프링 데이터 JPA는 2가지 방법으로 QueryDSL을 지원한다.

QueryDslPredicateExecutor 사용

리포지토리에서 QueryDslPredicateExecutor를 상속받으면 된다.
(org.springframework.data.querydsl.QueryDslPredicateExecutor)

puiblic interface ItemRepository
	extends JpaRepository<Item, Long>, QueryDslPredicateExecutor<Item> {
}

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

QueryDslPredicateExecutor 사용에는 join, fetch를 사용할 수 없다는 한계가 있다. (JPQL에서 이야기하는 묵시적 조인은 가능) 따라서 QueryDSL이 제공하는 다양한 기능을 사용하려면 JPAQuery를 직접 사용하거나 스프링 데이터 JPA가 제공하는 QueryDslRepositorySupport를 사용해야 한다.

QueryDslRepositorySupport

(org.springframework.data.querydsl.QueryDslRepositorySupport)

사용예시 - 주문내역 검색 기능

// CustomOrderRepository 사용자 정의 리포지토리
public interface CustomOrderRepository {
	public List<Order> search(OrderSearch orderSearch);
}

// QueryDslRepositorySupprot 사용 코드.
public class OrderRepositoryImpl extends QueryDslRepositorySupport
	implements CustomOrderRepository {
    
    public OrderRepositoryImpl() {
    	suprer(Order.class);  //엔티티 클래스 정보
    }
    
    @Override
    public List<Order> search(OrderSearch ordeerSearch) {
    
    	QOrder order = QOrder.order;
        QMember member = QMember.member;
        
        JPQLQuery query = from(order);
        
        if (StringUtils.hasTest(oderSearch.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에 엔티티 클래스 정보를 넘겨주어야 한다.

참조 : [자바 ORM 표준 JPA 프로그래밍]

profile
코드 위에서 춤추고 싶어요

0개의 댓글