BOOK 3 - 자바 ORM 표준 JPA 프로그래밍(11)
스프링 데이터 JPA
스프링 데이터 JPA 소개
- 스프링 데이터 JPA는 스프링 프레임워크에서 JPA를 편리하게 사용할 수 있도록 지원하는 프로젝트다.
- CRUD를 처리하기 위한 공통 인터페이스를 제공한다.
- CRUD를 처리하기 위한 공통 메서드를
JpaRepository인터페이스로 제공한다.
- CRUD를 처리하기 위한 공통 메서드를
Repository를 개발할 때 Interface만 작성하면 실행 시점에 스프링 데이터 JPA가 구현 객체를 동적으로 생성해서 주입해준다.- 즉, 데이터 접근 계층을 개발할 때 구현 클래스 없이 인터페이스만 작성해도 개발을 완료할 수 있다.
public interface MemberRepository extends JpaRepository<Member, Long> {
Member findByUsername(String username);
}
public interface ItemRepository extends JpaRepository<Item, Long> {
}
-
기본적인 CRUD 메소드는
JpaRepository가 제공한다. -
MemberRepository.findByUsername(...)같은 직접 작성한 메서드는 스프링 데이터 JPA가 메서드 이름을 분석해서 다음과 같이 JPQL을 실행한다.select m from Member m where username = :username
공통 인터페이스 기능
- 스프링 데이터 JPA를 사용하는 가장 단순한 방법은
JpaRepository인터페이스를 상속받는 것이다.- generic에 엔티티 클래스와 식별자 타입을 지정하면 된다.
public interface MemberRepository extends JpaRepository<Member, Long> {
}
JpaRepository 인터페이스의 계층 구조

- 스프링 데이터 모듈 안에 있는
Repository,CrudRepository,PagingAndSortingRepository는 스프링 데이터 프로젝트가 공통으로 사용하는 인터페이스다. JpaRepository는 여기에 추가로 JPA에 특화된 기능을 제공한다.save(S): 새로운 엔티티는 저장하고, 이미 있는 엔티티는 수정한다.- 엔티티에 식별자 값이
null이면 새로운 엔티티로 판단해서EntityManager.persist()를 호출하고, 식별자 값이 있으면 이미 있는 엔티티로 판단해서EntityManager.merge()를 호출한다.
- 엔티티에 식별자 값이
delete(T): 엔티티 하나를 삭제한다. 내부에서EntityManager.remove()를 호출한다.findOne(ID): 엔티티 하나를 조회한다. 내부에서EntityManager.find()를 호출한다.getOne(ID): 엔티티를 프록시로 조회한다. 내부에서EntityManager.getReference()를 호출한다.findAll(...): 모든 엔티티를 조회한다.- 정렬이나 페이징 조건을 파라미터로 넘길 수 있다.
쿼리 메소드 기능
- 스프링 데이터 JPA는 메소드 이름만으로 쿼리를 생성하는 기능을 제공한다.
- 즉, 인터페이스에 메소드만 선언하면 해당 메소드의 이름으로 적절한 JPQL 쿼리를 생성해서 실행한다.
- 스프링 데이터 JPA가 제공하는 쿼리 메소드 기능은 크게 3가지가 있다.
- 메소드 이름으로 쿼리 생성
- 메소드 이름으로 JPA NamedQuery 호출
@Query어노테이션을 사용해서 Repository interface에 쿼리 직접 정의
메소드 이름으로 쿼리 생성
public interface MemberRepository extends Repository<Member, Long> {
List<Member> findByEmailAndName(String email, String name);
}
findByEmailAndName(...)메서드를 실행하면 스프링 데이터 JPA는 메소드 이름을 분석해서 다음 JPQL을 생성하고 실행한다.
select m from Member m where m.email = ?1 and m.name = ?2
-
쿼리 생성 기능을 사용하려면 정해진 규칙에 따라서 메소드 이름을 지어야 한다.

JPA NamedQuery
- 스프링 데이터 JPA는 메소드 이름으로 JPA Named 쿼리를 호출하는 기능을 제공한다.
@Entity
@NamedQuery(name="Member.findByUsername",
query="select m from Member m where m.username = :username")
public class Member {
...
}
- 위에서 정의한 Named 쿼리를 JPA에서 직접 호출하려면 다음과 같이 작성해야 한다.
public class MemberRepository {
@PersistenceContext
EntityManager em;
public List<Member> findByUsername(String username) {
...
List<Member> resultList = em.createNamedQuery("Member.findByUsername", Member.class)
.setParameter("username", username)
.getResultList();
}
}
@PersistenceContext- 순수 자바 환경에서는
EntityManagerFactory에서 엔티티 매니저를 직접 생성해서 사용했다. - 스프링에서는 Spring Container가 엔티티 매니저를 관리하고 제공해준다.
- 해당 어노테이션으로 컨테이너가 관리하는 엔티티 매니저를 주입받아 사용해야 컨테이너가 제공하는 트랜잭션 기능과 연계해서 컨테이너의 다양한 기능들을 사용할 수 있다.
- 순수 자바 환경에서는
EntityManagerFactory를 주입 받으려면 @PersistenceUnit 어노테이션을 사용한다.
- 스프링 데이터 JPA를 사용하면 메소드 이름만으로 Named 쿼리를 호출할 수 있다.
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsername(@Param("username") String username);
}
- 스프링 데이터 JPA는 선언한
도메인 클래스 + . + 메소드 이름으로 Named 쿼리를 찾아서 실행한다.- 만약 해당하는 Named 쿼리가 없으면 메소드 이름으로 쿼리 생성 전략을 사용한다.
@Param: 이름 기반 파라미터 바인딩시에 사용하는 어노테이션
@Query 사용하여 Repository 메소드에 쿼리 정의
- Repository 메소드에 직접 쿼리를 정의하려면
@Query어노테이션을 사용한다.
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query("select m from Member m where m.username = ?1")
Member findByUsername(String username);
}
-
@Query어노테이션의 속성으로 실행할 JPQL 쿼리를 작성한다.- 네이티브 SQL을 사용하려면
nativeQuery = true를 설정한다. - JPQL은 위치 기반 파라미터를 1부터 시작하지만, 네이티브 SQL은 0부터 시작한다.
public interface MemberRepository extends JpaRepository<Member, Long> { @Query(value="SELECT * FROM MEMBER WHERE USERNAME = ?0", nativeQuery = true) Member findByUsername(String username); } - 네이티브 SQL을 사용하려면
파라미터 바인딩
- 스프링 데이터 JPA는 위치 기반 파라미터 바인딩과 이름 기반 파라미터 바인딩을 모두 지원한다.
- 이름 기반 파라미터 바인딩을 사용하려면
@Param어노테이션을 사용한다.
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query("select m from Member m where m.username = :name")
Member findByUsername(@Param("name") String username);
}
벌크성 수정 쿼리
- 스프링 데이터 JPA에서 벌크성 수정, 삭제 쿼리는
@Modifying어노테이션을 사용한다.
@Modifying
@Query("update Product p set p.price = p.price * 1.1 where p.stockAmount < :stockAmount")
int bulkPriceUp(@Param("stockAmoun") String stockAmount);
- 벌크성 쿼리 실행 후 영속성 컨텍스트를 초기화하고 싶으면
@Modifying(clearAutomatically = true)로 설정한다.
반환 타입
- 스프링 데이터 JPA는 조회 결과가 한 건 이상이면 컬렉션 인터페이스를 사용하고, 단 건이면 반환 타입을 지정한다.
List<Member> findByName(String name);
Member findByEmail(String email);
- 조회 결과가 없으면 컬렉션은 빈 컬렉션을, 단건은
null을 반환한다.- 단 건으로 지정한 메소드를 호출하면 스프링 데이터 JPA는 내부적으로
Query.getSingleResult()메서드를 호출한다.
- 단 건으로 지정한 메소드를 호출하면 스프링 데이터 JPA는 내부적으로
- 단 건 조회를 위해 반환 타입을 지정했는데 결과가 2건 이상 조회되면
NonUniqueResultException예외가 발생한다.
페이징과 정렬
- 스프링 데이터 JPA는 쿼리 메소드에 페이징과 정렬 기능을 사용할 수 있도록 아래 2가지 파라미터를 제공한다.
Sort: 정렬 기능Pageable: 페이징 기능- 반환 타입으로
List나Page를 선택할 수 있다. Page타입으로 반환 시 스프링 데이터 JPA는 페이징 기능을 제공하기 위해 검색된 전체 데이터 건수를 조회하는COUNT쿼리를 추가로 수행한다.
- 반환 타입으로
Page<Member> findByName(String name, Pageable pageable);
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생성자의 첫번째 파라미터는 현재 페이지, 두번째 파라미터는 조회할 데이터 수를 입력한다.- 페이지는 0부터 시작한다.
Lock
- 쿼리 시 락을 걸려면
@Lock어노테이션을 사용한다.
@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findByName(String name);
명세 (Specification)
- 스프링 데이터 JPA는 JPA Criteria로 Specification 개념을 사용할 수 있도록 지원한다.
- Specification을 이해하기 위한 핵심 단어는
Predicate인데, 이것은 단순히 참이나 거짓으로 평가된다.AND나OR같은 연산자로 조합할 수 있다.- 예를 들어, 데이터를 검색하기 위한 제약 조건 하나하나를 Predicate 라 할 수 있다.
- 이 Predicate를 스프링 데이터 JPA는
Specification클래스로 정의했다.Specification은 COMPOSITE 패턴으로 구성되어 있어서 여러Specification을 조합할 수 있다.- 따라서 여러 검색 조건을 조합해서 새로운 검색조건을 만들 수 있다.
- Specification 기능을 사용하려면 Repository에서
JpaSpecificationExecutor인터페이스를 상속받으면 된다.
public interface OrderRepository extends JpaRepository<Order, Long>, JpaSpecificationExecutor<Order> {
}
JpaSpecificationExecutor의 메소드들은Specification을 파라미터로 받아서 검색 조건으로 사용한다.
public List<Order> findOrders(String name) {
List<Order> result = orderRepository.findAll(
where(memberName(name)).and(isOrderStatus())
);
return result;
}
findAll()메소드에 회원 이름 명세(memberName)와 주문 상태 명세(isOrderStatus)를and로 조합해서 검색 조건 파라미터로 넘겨주었다.
명세 정의
- 명세를 정의하려면
Specification인터페이스를 구현하면 된다.- 인터페이스 구현시에
toPredicate()메서드만 구현하면 되는데 파라미터로 주어지는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 Specification<Order> isOrderStatus() {
return new Specification<Order>() {
public Predicate toPredicate(Root<Order> root, CriteriaQuery<?> Query, CriteriaBuilder builder) {
return builder.equal(root.get("status"), OrderStatus.ORDER);
}
}
}
}
사용자 정의 Repository 구현
- 사용자가 직접 Repository의 메소드를 구현하여야 할 때 사용자 정의 인터페이스를 생성한 뒤 해당 인터페이스를 구현하는 클래스를 작성해야 한다.
public interface MemberRepositoryCustom {
public List<Member> findMemberCustom();
}
- 사용자 정의 인터페이스를 구현하는 클래스는 이름을 짓는 규칙이 있는데,
Repository 인터페이스 이름 + Impl로 지어야한다.- 규칙에 맞게 이름을 지어야 스프링 데이터 JPA가 사용자 정의 구현 클래스로 인식한다.
public class MemberRepositoryImpl implements MemberRepositoryCustom {
@Override
public List<Member> findMemberCustom() {
// 사용자 정의 구현
...
}
}
- 마지막으로 Repository 인터페이스에서 사용자 정의 인터페이스를 상속받으면 된다.
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
}
스프링 데이터 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(entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
...
}
@Repository: JPA Exception을 스프링이 추상화한 Exception으로 변환한다.@Transactional- JPA의 모든 변경은 트랜잭션 안에서 이루어져야 한다.
- 스프링 데이터 JPA가 제공하는 공통 인터페이스를 사용하면 데이터 등록/수정/삭제 하는 메소드에 이미 트랜잭션 처리가 되어 있다.
- Service Layer에서 트랜잭션을 시작하지 않으면 Repository에서 트랜잭션을 시작한다.
@Transactional(readOnly = true)- 데이터를 조회하는 메소드에는
readOnly = true옵션이 적용되어 있다. - 데이터를 조회만 하는 트랜잭션에서 이 옵션을 사용하면 플러시를 생략해서 약간의 성능 향상을 얻을 수 있다.
- 데이터를 조회하는 메소드에는
스프링 데이터 JPA와 QueryDSL 통합
- 스프링 데이터 JPA는 2가지 방법으로 QueryDSL을 지원한다.
QueryDslPredicateExecutorQueryDslRepositorySupport
QueryDslPredicateExecutor 사용
- Repository에서
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을 검색 조건으로 사용하면서 스프링 데이터 JPA가 제공하는 페이징과 정렬 기능도 함께 사용할 수 있다.
public interface QueryDslPredicateExecutor<T> { ... Iterable<T> findAll(Predicate predicate); Page<T> findAll(Predicate predicate, Pageable pageable); } -
QueryDslPredicateExecutor를 사용하는 방법으로는join,fetch를 사용할 수 없다.
QueryDslRepositorySupport 사용
- QueryDSL의 모든 기능을 사용하려면
QueryDslRepositorySupport를 상속받아 사용한다. - 스프링 데이터 JPA가 제공하는 공통 인터페이스는 메소드를 직접 구현할 수 없기 때문에 사용자 정의 Repository를 생성한다.
public interface CustomOrderRepository {
public List<Order> search(OrderSearch orderSearch);
}
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);
}
}
- 검색 조건인
OrderSearch값에 따라 동적으로 쿼리를 생성한다. QueryDslRepositorySupport를 상속받을 때 생성자에서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<JPAUpdateClause> update(EntityPath<?> path) {
return new JPAUpdateClause(entityManager, path);
}
...
}