JPA 프로그래밍
Ch12. 스프링 데이터 JPA
developer-soyun
2025. 1. 22. 11:48
12.1 스프링 데이터 JPA 소개
스프링 데이터 JPA는 스프링 프레임워크에서 JPA를 편리하게 사용할 수 있도록 지원하는 프로젝트이다
- 데이터 접근 계층을 개발할 때 구현 클래스 없이 인터페이스만 작성해도 개발을 완료할 수 있다
public interface MemberRepository extends JPAResponse<Member, Long>{
Member findByUsername(String username);
}
스프링 데이터 JPA 적용
12.1.1 스프링 데이터 프로젝트
- 스프링 데이터 프로젝트는 데이터 저장소에 대한 접근을 추상화해서 개발자 편의을 제공하고 지루하게 반복하는 데이터 접근 코드를 줄여준다
12.4 쿼리 메서드 가능
인터페이스에 메서드만 선언하면 해당 메서드의 이름으로 적절한 JPQL 쿼리를 생성해서 실행한다
스프링 데이터 JPA가 제공하는 쿼리 메서드 기능은 크게 3가지가 있다
- 메서드 이름으로 쿼리 생성
- 메서드 이름으로 JPA NamedQuery 호출
- @Query 어노테이션을 사용해서 리포지토리 인터페이스에 쿼리 직접 정의
12.4.1 메서드 이름으로 쿼리 생성
public interface MemberRepository extends Repository<Member, Long>{
List<Member> findByEmailAndName(String email, String name);
}
- 스프링 데이터 JPA는 메서드 이름을 분석해서 JPQL을 생성하고 실행한다
12.4.2 JPA NamedQuery
@Entity
@NamedQuery{
name = "Member.findByUsername",
query = "select m from Member mwhere m.username = :username")
public class Member{
...
}
@NamedQuery 어노테이션으로 Named 쿼리 정의
- 쿼리에 이름을 부여해서 사용하는 방법이다
- 어노테이션이나 XML 에 쿼리를 정의할 수 있다
public class MemberRepository{
public List<Member> findByUsername(String username){
...
List<Member> resultList =
em.createNamedQuery("Member.findByUsername", Member.class)
.setParameter("username", "회원")
.getResultList();
}
}
JPA를 직접 사용해서 Named 쿼리 호출
public interface MemberRepository extends JPARepository<Member, Long>{
List<Member> findByUsername(@Param("username") String username);
}
스프링 데이터 JPA로 Named 쿼리 호출
- 스프링 데이터 JPA 는 선언한 "도메인 클래스 + .(점) + 메서드 이름" 으로 NamedQuery 를 찾아서 실행한다
12.4.3 @Query, 리파지토리 메서드에 쿼리 정의
public interface MemberRepository extends JPARepository<Member,Long>{
@Query("select m from Member m where m.username = ?1")
Member findByUsername(String username);
}
메서드에 JPQL 쿼리 작성
- 애플리케이션 실행 시점에 문법 오류를 발견할 수 있다
public interface MemberRepository extends JPARepository<Member,Long>{
@Query(value = "SELECT * FROM MEMBER WHERE USERNAME = ?0", nativeQuery = true)
Member findbyUsername(String username);
}
JPA 네이티브 SQL 지원
- 네이티브 SQL을 사용하려면 @Query 어노테이션에 nativeQuery = true 를 설정한다
- 스프링 데이터 JPA는 위치 기반 파라미터를 1부터 시작하지만 네이티브 SQL은 0부터 시작한다
12.4.4 파라미터 바인딩
스프링 데이터 JPA는 위치 기반 파라미터 바인딩과 이름 기반 파라미터 바인딩을 모두 지원한다
기본값은 위치 기반이다
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.4.6 반환 타입
- 결과가 한 건 이상이면 컬렉션 인터페이스를 사용하고, 단건이면 반환 타입을 지정한다
- 만약 조회 결과가 없으면 컬렉션은 빈 컬렉션을 반환하고 단건은 null을 반환한다
- 단건을 기대하고 반환 타입을 지정했는데 결과가 2건 이상 조회되면 예외가 발생한다
- javax.persistence.NonUniqueResultException
12.4.7 페이징과 정렬
스프링 데이터 JPA는 쿼리 메서드에 페이징과 정렬 기능을 사용할 수 있도록 2가지 특별한 파라미터를 제공한다
- org.springframework.data.domain.Sort : 정렬기능
- org.springframework.data.domain.Pageable : 페이징 기능 (내부에 Sort 포함)
public interface MemberRepository extends JpaRepository<Member, Long>{
Page<Member> findByNameStartingWith(String name, Pageable pageable);
}
Page 사용 예제 정의 코드
//페이징 조건과 정렬 조건 설정
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(); //다음 페이지 존재 여부
Page 사용 예제 실행 코드
12.5 명세
명세(Specification)을 이해하기 위한 핵심 단어는 술어(predicate)인데 이것은 단순히 참이나 거짓으로 평가된다. 그리고 이것은 AND나 OR 같은 연산자로 조합할 수 있다
- Specification은 컴포지트 패턴으로 구성되어 있어서 여러 Specification 을 조합할 수 있다. 따라서 다양한 검색조건을 조립해서 새로운 검색 조건을 쉽게 만들 수 있다
public interface OrderRepository extends JpaRepository<Order, Long>, JpaSpecificationExecutor<Order>{
}
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);
}
JpaSpecificationExecutor 인터페이스
import static org.springframeword.data.jpa.domain.Specification.*; //where()
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;
}
명세 사용 코드
- Specifications 는 명세들을 조립할 수 있도록 도와주는 클래스인데 where(), and(), or(), not() 메서드를 제공한다
12.6 사용자 정의 리포지토리 구현
리파지토리를 직접 구현하면 공통 인터페이스가 제공하는 기능까지 모두 구현해야한다. 스프링 데이터 JPA는 이런 문제를 우회해서 필요한 메서드만 구현할 수 있는 방법을 제공한다
- 사용자 정의 인터페이스 작성
- 사용자 정의 인터페이스를 구현한 클래스 작성
- 리포지토리 인터페이스 이름 +Impl 로 지어야한다 => 이렇게 해야 스프링 데이터 JPA가 사용자 정의 구현 클래스로 인식한다
- 리파지토리 인터페이스에서 사용자 정의 인터페이스를 상속
import java.util.Optional;
public interface UserRepositoryCustom {
Optional<User> findUserByCustomCondition(String condition);
}
사용자 정의 인터페이스
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.TypedQuery;
import java.util.Optional;
public class UserRepositoryCustomImpl implements UserRepositoryCustom {
@PersistenceContext
private EntityManager entityManager;
@Override
public Optional<User> findUserByCustomCondition(String condition) {
String jpql = "SELECT u FROM User u WHERE u.conditionField = :condition";
TypedQuery<User> query = entityManager.createQuery(jpql, User.class);
query.setParameter("condition", condition);
return query.getResultStream().findFirst();
}
}
사용자 정의 구현 클래스
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long>, UserRepositoryCustom {
}
사용자 정의 인터페이스 상속
12.7 Web 확장
12.7.2 도메인 클래스 컨버터 기능
도메인 클래스 컨버터는 HTTP 파라미터로 넘어온 엔티티의 아이디로 엔티티 객체를 찾아서 바인딩해준다
@Controller
public class MemberController{
@RequestMapping("member/memberUpdateForm")
public String memberUpdateForm(@RequestParam("id") Member member, Model model){
model.addAttribute("member", member);
return "member/memberSaveForm";
}
도메인 클래스 컨버터 적용
- HTTP 요청으로 회원 아이디(id)를 받지만 도메인 클래스 컨버터가 중간에 동작해서 아이디를 회원 엔티티 객체로 변환해서 넘겨준다. 따라서 컨트롤러를 단순하게 사용할 수 있다
12.7.3 페이징과 정렬 기능
- 페이징기능 : 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은 다음 요청 파라미터 정보로 만들어진다
- page : 현재 페이지, 0부터 시작
- size : 한 페이지에 노출할 데이터 건수
- sort : 정렬 조건을 정의한다
Pageable의 기본값은 page = 0, size = 20 이다. 만약 기본값을 변경하고 싶으면 @PageableDefault 어노테이션을 사용하면 된다
@RequestMapping(value = "/members", method = RequestMethod.GET)
public String list(@PageableDefault(size=12, sort="name", direction =
Sort.Direction.DESC) Pageable pageable){
...
}
12.10 스프링 데이터 JPA와 QueryDSL 통합
스프링 데이터 JPA는 2가지 방법으로 QuryDSL을 지원한다
- org.springframework.data.querydsl.QueryDslPredicateExecutor
- org.springframework.data.querydsl.QueryDslRepositorySupport
12.10.1 QueryDslPredicateExecutor 사용
public interface ItemRepository extends JpaRepository<Item, Long>, QueryDslPredicateExecutor<Item>{
}
QItem item = QItem.item;
Iterable<Item> result = itemRepository.findAll(
item.name.contains("장난감").and(item.price.between(10000,20000))
);
QueryDSL 사용 예제
- QueryDslPredicateExecutor는 스프링 데이터 JPA에서 관리하여 QueryDSL을 사용할 수 있지만 기능에 한계가 있다
12.10.2 QueryDslRepositorySupport 사용
- QueryDSL의 모든 기능을 사용하려면 JPAQuery 객체를 직접 생성해서 사용하면 된다.
- 이때 QueryDslRepositorySupport를 상속 받아 사용하면 조금 더 편리하게 QueryDSL을 사용할 수 있다
import java.util.List;
public interface UserRepositoryCustom {
List<User> findByName(String name);
}
사용자 정의 리파지토리
import com.querydsl.jpa.impl.JPAQueryFactory;
import org.springframework.stereotype.Repository;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.List;
public class UserRepositoryImpl extends QueryDslRepositorySupport implements UserRepository{
@PersistenceContext
private EntityManager entityManager;
public UserRepositoryImpl() {
super(User.class);
}
public List<User> findByName(String name) {
QUser user = QUser.user;
JPAQueryFactory queryFactory = new JPAQueryFactory(entityManager);
return queryFactory.selectFrom(user)
.where(user.name.eq(name))
.fetch();
}
}
리파지토리 구현