스프링 데이터 JPA
- 스프링 데이터 JPA는 스프링 데이터 프로젝트의 하위 프로젝트 중 하나로 스프링 프레임워크에서 JPA를 쉽게 사용할 수 있게 지원한다.
- 인터페이스만 작성하면 실행 시점에 스프링 데이터 JPA가 구현 객체를 동적으로 생성해서 주입해준다. 그래서 데이터 접근 계층을 구현 클래스 없이 인터페이스 작성만으로 개발할 수 있다.
공통 인터페이스 기능
스프링 데이터 JPA는 간단한 CRUD 기능을 공통으로 처리하는 JpaRepository 인터페이스를 제공한다. JpaRepository는 스프링 데이터 JPA의 특화된 기능을 제공한다.
public interface JpaRepository<T, ID extends Serializable> extends PagingAndSortingRepository<T, ID> {
...
}
public interface MemberRepository extends JpaRepository<Member, Long> {
...
}
JpaRepository 주요 메서드
T(엔티티), ID(엔티티의 식별자 타입), S(엔티티와 그 자식 타입)
메서드명 | 설명 |
---|---|
save(S) |
새로운 엔티티를 저장하고 이미 있는 엔티티는 수정한다. 엔티티에 식별자 값이 없으면 EntityManager.persist() , 식별자 값이 있으면 EntityManager.merge() 호출 |
delete(T) |
엔티티 하나를 삭제한다. EntityManager.remove() 호출 |
findOne(ID) |
엔티티 하나를 조회한다. EntityManager.find() 호출 |
getOne(ID) |
엔티티를 프록시로 조회한다. EntityManager.getReference() 호출 |
findAll(...) |
모든 엔티티를 조회한다. 정렬이나 페이징 조건을 파라미터로 제공 |
쿼리 메서드 기능
메서드 이름으로 쿼리 생성
작성한 메서드 이름을 분석하여 JPQL 실행
public interface MemberRepository extends JpaRepository<Member, Long> {
Member findByEmailAndName(String email, String name);
}
// JPQL
SELECT m FROM Member m WHERE m.email = ?1 AND m.name = ?2
Spring Data JPA-Query Method 공식 문서
NamedQuery 호출
- 애플리케이션 실행 시점에 문법 오류를 발견할 수 있다.
클래스명.메서드
로 Named 쿼리를 먼저 찾아서 실행한다. 만약 Named 쿼리가 존재하지 않으면 메서드 이름으로 쿼리 생성 전략을 사용한다.
@Entity
@NamedQuery(
name="Member.findByUsername",
query="selct m from Member m where m.username = :username"
)
public class Member {
...
}
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsername(@Param("username") String username);
}
@Query 애너테이션 사용
@Query
애너테이션을 사용하여 레포지토리 인터페이스에 직접 쿼리를 정의한다.- JPA Named 쿼리처럼 애플리케이션 실행 시점에 문법 오류를 발견할 수 있다.
- 네이티브 SQL을 사용하려면
@Query
애너테이션에nativeQuery=true
속성을 설정하면 된다.
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query("select m from Member m where m.username = ?1")
List<Member> findUser(String username);
@Query(value = "SELECT * FROM MEMBER WHERE EMAIL = ?0",
nativeQuery = true)
List<Member> findByEmail(String email);
}
스프링 데이터 JPA의 위치 기반 파라미터 바인딩의 경우 JPQL은 파라미터를 1부터, 네이티브 SQL은 0부터 시작한다.
파라미터 바인딩
스프링 데이터 JPA는 위치 기반, 이름 기반 파라미터 바인딩을 지원한다. 기본값은 위치 기반 파라미터 바인딩을 사용한다. 코드 가독성과 유지보수를 위해 이름 기반 파라미터 바인딩을 사용하자.
public interface MemberRepository extends JpaRepository<Member, Long> {
// 위치 기반
@Query("select m from Member m where m.username = ?1")
List<Member> findUser(String username);
// 이름 기반
@Query("select m from Member m where m.username = :name")
List<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("stockAmount") String stockAmount);
쿼리 후 영속성 컨텍스트를 초기화하려면 @Modifying(clearAutomatically=true)
설정
반환 타입
스프링 데이터 JPA는 유연한 반환 타입을 지원한다.
List<Member> findByUsername(String name);
Member findByEmail(String email);
- 조회 결과가 없으면 컬렉션은 빈 컬렉션, 단 건은 null을 반환한다.
- 단 건으로 설정했는데 2건 이상이 조회되면
NonUniqueResultException
예외가 발생한다.
페이징과 정렬
스프링 데이터 JPA는 쿼리 메서드에 페이징과 정렬을 사용 가능한 파라미터를 제공한다.
org.springframework.data.domain.Sort
: 정렬 기능org.springframework.data.domain.Pageable
: 페이징 기능(내부에 Sort 포함)
파라미터에 Pageable을 사용하면 반환 타입으로 List나 Page를 사용할 수 있다. 이 때 반환 타입이 Page면 페이징 기능을 제공하기 위해 전체 데이터 건수 조회 쿼리를 추가로 호출한다.
public interface MemberRepository extends Repository<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);
힌트
SQL에서 힌트를 사용하듯이 JPA에서도 JPA 구현체에 힌트를 전달할 수 있다.
기본적으로 JPA를 이용해 데이터를 조회하면 영속성 컨텍스트에 저장되어 관리되고, 그 값을 수정한 뒤 flush()
하거나 변경 감지가 발생하면 UPDATE 쿼리도 발생하게 된다. 이 때 힌트를 사용하여 영속성 컨텍스트에 저장되는 것을 방지해 메모리 낭비를 막고 혹시 모를 변경사항에 대해 업데이트 되지 않도록 설정할 수 있다.
public interface MemberRepository extends Repository<Member, Long> {
@QueryHints(value = {@QueryHint(name="org.hibernate.readOnly", value="true")}, forCounting=true)
Page<Member> findByName(String name, Pageable pageable);
}
변경사항이 DB에 반영될 필요 없이 오직 조회만 필요할 때 @QueryHint(name="org.hibernate.readOnly", value="true")
옵션을 지정하여 사용할 수 있다. 그러나 힌트로 극적인 효과를 보기는 어렵다. 단순 조회인데 부하가 심한 API나 상당히 빈번하게 호출되는 경우 등에 한해 적용해보고 이점이 있는 경우에만 적용하는 게 바람직하다.
forCounting=true
옵션을 지정하면 반환 타입으로 Page 인터페이스을 사용했을 때, 추가로 count 쿼리에도 쿼리 힌트를 적용한다.
Lock
JPA의 기본 동작이 select - update
이기 때문에 어떤 값을 동시에 여러 스레드에서 변경하려고 할 때 그 값의 정합성을 보장하기 어렵다. 이 때 락을 사용하여 해결할 수 있다.
@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findByName(String name);
Specification
스프링 데이터에서 Specification은 DB 쿼리의 조건을 Spec으로 작성해 Repository 메서드에 적용하거나 몇가지 Spec을 조합하여 사용할 수 있게 도와준다.
// JpaSpecificationExecutor의 메서드들은 Specification 파라미터를 검색 조건으로 사용한다.
public interface JpaSpecificationExecutor<T> {
Optional<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);
}
public interface OrderRepository extends JpaRepository<Order, Long>, JpaSpecificationExecutor<Order> {
...
}
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);
}
}
}
}
public List<Order> findOrders(String name) {
List<Order> resultList = orderRepository.findAll(
where(OrderSpec.memberName(name))
.and(OrderSpec.isOrderStatus())
);
return result;
}
사용자 정의 레포지토리 구현
스프링 데이터 JPA는 필요한 메서드만 구현할 수 있는 방법을 제공한다.
public interface MemberRepositoryCustom {
public List<Member> findMemberCustom();
}
public class MemberRepositoryImpl implements MemberRepositoryCustom {
@Override
public List<Member> findMemberCustom() {
...
}
}
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
}
@EnableJpaRepositories(basePackages="com.spring.repository", repositoryImplementationPostfix="Impl")
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
구현 클래스 이름 끝에 Impl 대신 다른 이름으로 변경하고자 할 때 repositoryImplementationPostfix
속성(기본값 Impl)을 변경하면 된다.
Web 확장
- 스프링 데이터 프로젝트는 스프링 MVC에서 사용할 수 있는 편리한 기능을 제공한다.
설정
스프링 데이터가 제공하는 Web 확장 기능을 활성화하려면 org.springframework.data.web.config.SpringDataWebConfiguration
스프링 빈을 등록해야 한다.
@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
public class WebAppConfig {
...
}
설정을 완료하면 도메인 클래스 컨버터와 페이징, 정렬을 위한 HandlerMethodArgumentResolver
가 스프링 빈으로 등록된다.
도메인 클래스 컨버터
HTTP 파라미터로 넘어온 엔티티 아이디로 엔티티 객체를 찾아 바인딩해주는 기능을 수행한다.
// 기존 사용 방법
@Controller
@AllArgsConstructor
public class MemberController {
private MemberRepository memberRepository;
@GetMapping("/member/{id}")
public String detailView(@PathVariable("id") Long id, Model model) {
Member member = memberRepository.findOne(id);
model.addAttribute("member", member);
...
}
}
// 도메인 클래스 컨버터 적용
@Controller
public class MemberController {
@GetMapping("/member/{id}")
public String detailView(@PathVariable("id") Member member, Model model) {
model.addAttribute("member", member);
...
}
}
- 도메인 클래스 컨버터가 동작해 아이디를 회원 엔티티 객체로 변환하여 전달한다. 이 때 해당 엔티티와 관련된 레포지토리를 사용해 엔티티를 탐색한다.
- 도메인 클래스 컨버터를 통해 넘어온 회원 엔티티는 영속성 컨텍스트의 동작방식(OSIV)으로 인해 직접 수정해도 DB에 반영되지 않는다.
페이징과 정렬
스프링 데이터의 페이징과 정렬 기능을 스프링 MVC에서 편리하게 사용할 수 있도록 HandlerMethodArgumentResolver
를 통해 제공한다.
PageableHandlerMethodArgumentResolver
(페이징), SortHandlerMethodArgumentResolver
(정렬)
@GetMapping("/members")
public String listView(Pageable pageable, Model model) {
Page<Member> page = memberService.findMembers(pageable);
model.addAttribute("members", page.getContent());
...
}
파라미터로 Pageable을 받고 Pageable은 다음 요청 파라미터로 정보 생성
/members?page=0&size=20&sort=name,desc&sort=address.city
- page : 현재 페이지
- size : 한 페이지의 데이터 건 수
- sort : 정렬 조건 정의
접두사
사용해야 할 페이징 정보가 둘 이상일 때 접두사를 사용하여 구분할 수 있다.
/members?member_page=0&order_page=1
@GetMapping("/members")
public String listView(
@Qualifier("member") Pageable memberPageable,
@Qualifier("order") Pageable orderPageable), ...
)
기본값
Pageable
의 기본값을 변경하고자 할 때 @PageableDefault
애너테이션을 사용한다.
@GetMapping("/members")
public String listView(@PageableDefault(size=12, sort="name", direction=Sort.Direction.DESC) Pageable pageable) {
...
}
스프링 데이터 JPA 구현체
스프링 데이터 JPA가 제공하는 공통 인터페이스는 SimpleJpaRepository
클래스가 구현한다.
@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
@Transactional
@Override
public <S extends T> S save(S entity) {
Assert.notNull(entity, "Entity must not be null.");
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
}
메서드 및 에너테이션 | 설명 |
---|---|
@Repository |
JPA 예외를 스프링이 추상화한 예외로 변환한다. |
@Transactional(readOnly = true) |
전체적으로 트랜잭션이 적용되어 있다. 이로 인해 서비스에서 트랜잭션을 적용하지 않으면 레포지토리에서 트랜잭션을 시작하게 된다. 이 옵션을 사용하면 플러시를 생략하기 때문에 약간의 성능 향상 효과가 있다. |
save() 의 @Transactional |
조회 메서드가 아닌 곳에는 readOnly=true 옵션을 적용하지 않는다. |
save() |
저장할 엔티티가 새로운 엔티티면 저장(persist )하고 이미 존재하는 엔티티면 병합(merge ) 한다. Persistable 인터페이스를 구현한 객체를 빈으로 등록하여 판별로직을 변경할 수 있다. |
스프링 데이터 JPA와 QueryDSL
스프링 데이터 JPA는 2가지 방법으로 QueryDSL을 지원한다.
org.springframework.data.querydsl.QuerydslPredicateExecutor
org.springframework.data.querydsl.QuerydslRepositorySupport
QuerydslPredicateExecutor
레포지토리에서 QuerydslPredicateExecutor
를 상속받아서 사용한다.
public interface itemRepository extends JpaRepository<Item, Long>, QuerydslPredicateExecutor<Item> {
}
QItem item = QItem.item;
Iterable<Item> result = itemRepository.findAll(
item.name.contains("ITEM1")
.and(item.price.between(10000, 20000)));
QuerydslPredicateExecutor
는 편리하게 QueryDSL을 사용할 수 있지만 join, fetch 를 사용하지 못 한다는 한계가 존재한다.
QuerydslRepositorySupport
QueryDSL의 모든 기능을 사용하려면 JPAQuery
객체를 직접 사용해야 한다. 스프링 데이터 JPA에서는 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.fetch();
}
}
Comments