12.1 스프링 데이터 JPA 소개
- 스프링 데이터 JPA는 스스프링 프레임워크에서 JPA를 편리하게 사용할 수 있도록 지원하는 프로젝트다. 이 프로젝트는 데이터 접근 계층을 개발할 때 지루하게 반복되는 CRUD 문제를 세련된 방법으로 해결한다. 우선 CRUD를 처리하기 위한 공통 인터페이스를 제공한다. 그리고 리포지토리를 개발할 때 인터페이스만 작성하면 실행 시점에 스프링 데이터 JPA가 구현 객체를 동적으로 생성해서 주입해준다. 따라서 데이터 접근 계층을 개발할 때 구현 클래스 없이 인터페이스만 작성해도 개발을 완료할 수 있다. CRUD를 처리하기 위한 공통 메소드는 스프링 데이터 JPA가 제공하는 org.springframework.data.jpa.repository.JpaRepository 인터페이스에 있다.
- 스프링 데이터 JPA는 스프링 뎅이터 프로젝트의 하위 프로젝트 중 하나다. 스프링 데이터 프로젝트는 JPA, 몽고DB, NEO4J, REDIS, HADOOP, GEMFIRE 같은 다양한 데이터 저장소에 대한 접근을 추상화해서 개발자 편의를 제공하고 지루하게 반복하는 데이터 접근 코드를 줄여준다.
- 스프링 데이터 JPA 프로젝트는 JPA에 특화된 기능을 제공한다. 스프링 프레임워크와 JPA를 함께 사용한다면 스프링 데이터 JPA 사용을 적극 추천한다.
12.2 스프링 데이터 JPA 설정
- 필요한 라이브러리
- 환경설정
: 스프링 데이터 jpa는 애플리케이션을 실행할 때 basePackage에 있는 리포지토리 인터페이스들을 찾아서 해당 인터페이스를 구현한 클래스를 동적으로 생성한다음 스프링 빈으로 등록한다. 따라서 개발자가 직접 구현 클래스를 만들지 않아도 된다.
12.3 공통 인터페이스 기능
- JpaRepository 인터페이스의 계층 구조
12.4 쿼리 메소드 기능
- 메소드 이름으로 쿼리 생성
: 스프링 데이터 JPA는 메소드 이름을 분석해서 JPQL을 생성하고 실행한다.
: 엔티티의 필드명이 변경되면 인터페이스에 정의한 메소드 이름도 꼭 함께 변경해야 한다. 그렇지 않으면 애플리케이션을 시작하는 시점에 오류가 발생한다.
- 메소드 이름으로 JPA NamedQuery 호출
: 스프링 데이터 JPA는 메소드 이름으로 JPA Named 쿼리를 호출하는 기능을 제공한다. 쿼리에 이름을 부여해서 사용하는 방법이다.
@Entity
@NamedQuery(
name="Member.findByUsername",
query="select m from Member m where m.username = :username")
public class Member {
...
}
// JPA를 직접 사용해서 Named 쿼리 호출
public class MemberRepository {
public List<Mmeber> findByUsername(String username) {
List<Member> resultList =
em.createNamedQuery("Member.findByUsername", Member.class)
.setParameter("username", "회원1")
.getResultList();
}
}
// @Param은 이름기반 파라미터를 바인딩할 때 사용하는 어노테이션
public interface MemberRepository extends JpaRepository<Member, Long> {
List<Member> findByUsername(@Param("username") String username);
}
- @Query 어노테이션을 사용해서 리포지토리 인터페이스에 쿼리 직접 정의
: 리포지토리 메소드에 직접 쿼리를 정의하려면 @org.springframework.data.jpa.repository.Query 어노테이션을 사용한다. 이 방법은 실행할 메소드에 정적 쿼리를 직접 작성하므로 이름 없는 Named 쿼리라 할 수 있다. 또한 JPA Named 쿼리처럼 애플리케이션 실행 시점에 문법 오류를 발견할 수 있는 장점이 있다.
: 네이티브 SQL을 사용하려면 @Query 어노테이션에 nativeQuery = true를 설정한다. 스프링 데이터 JPA가 지원하는 파라미터 바인딩을 사용하면 JPAL은 위치 기반 파라미터를 1부터 시작하지만 네이티브 SQL은 0부터 시작한다.
public interface MemberRepository extends JpaRepository<Member, Long> {
@Query(value = "SELECT * FROM MEMBER WHERE USERNAME = ?0", nativeQuery = true)
Member findByUsername(String username);
}
- 스프링 데이터 JPA는 위치 기반 파라미터 바인딩과 이름 기반 파라미터 바인딩을 모두 지원한다. 기본값은 위치 기반인데 파라미터 순서로 바인딩한다. 이름 기반 파라미터 바인딩을 사용하려면 @Param 어노테이션 사용하면 된다. 코드 가독성과 유지보수를 위해 이름 기반 파라미터 바인딩을 사용하자.
select m from Member m where m.username = ?1 // 위치 기반
select m from Member m where m.username = :name // 이름 기반
- 벌크성 수정 쿼리
: 스프링 데이터 JPA에서 벌크성 수정, 삭제 쿼리는 @Modifying 어노테이션을 사용하면 된다. 벌크성 쿼리를 실행하고 나서 영속성 컨텍스트를 초기화하고 싶으면 @Modifying(clearAutomatically = true)처럼 clearAtomatically 옵션을 true로 설정하면 된다. 이 옵션의 기본값은 false다.
int bulkPriceUp(String stockAmount) {
String qlString = "update Product p set p.rpcie = p.price * 1.1 where p.stockAmount < :stockAmount";
int resultCount = em.createQuery(qlString)
.setParameter("stockAmount", stockAmount)
.executeUpdate();
}
@Modifying
@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는 내부에서 JPQL의 Query.getSingleResult() 메소드를 호출한다. 이 세모드를 호출했을 때 조회 결과가 없으면 javax.persistence.NoResultException 예외가 발생하는데 개발자 입장에서 다루기가 상당히 불편하다. 스프링 데이터 JPA는 단건을 조회할 때 이 예외가 발생하면 예외를 무시하고 대신에 null을 반환한다.
List<Member> findByName(String name); // 컬렉션
Member findByEmail(String email); // 단건
- 페이징과 정렬
: 파라미터에 Pageable을 사용하면 반환 타입으로 List나 org.springframework.data.domain.Page를 사용할 수 있다. 반환 타입으로 Page를 사용하면 스프링 데이터 JPA는 페이징 기능을 제공하기 위해 검색된 전체 데이터 건수를 조회하는 count 쿼리를 추가로 호출한다.
: Pageable을 구현한 PageRequset 객체를 사용하고, PageRequset 생성자 첫 번째 파라미터는 현재 페이지, 두 번째 파라비터는 조회할 데이터 수, 세번째 파라미터에는 추가로 정렬 정보를 넣을 수 있다. 페이지는 0부터 시작한다.
org.springframework.data.domain.Sort: 정렬 기능
org.springframework.data.domain.Pageable: 페이징 기능(내부에 Sort포함)
// count 쿼리 사용
Page<Member> findByName(String name, Pageable pageable);
// count 쿼리 사용 안함
List<Member> findByName(String name, Pageable pageable);
List<Member> findByName(String name, Sort sort);
- 힌트
: JPA 쿼리 힌트를 사용하려면 org.springframeqwork.data.jpa.repository.QueryHints 어노테이션을 사용하면 된다. 참고로 이것은 SQL 힌트가 아니라 JPA 구현체에게 제공하는 힌트다.
// forCounting 속성은 반환 타입으로 Page 인터페이스를 적용하면 추가로 호출하는 페이징을 위한 count 쿼리에도 쿼리 힌트를 적용할지를 설정하는 옵션이다.
@QueryHints(value = { @QueryHint(name = "org.hibernate.readOnly", value = "true")}, forCounting = true)
Page<Mebmer> findByName(String name, Pageable pageable);
- Lock
: 쿼리 시 락을 걸려면 @Lock 어노테이션을 사용하면 된다.
@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findByName(String name);
12.5 명세
- 명세
: 명세(Specification)를 이해하기 위한 핵심 단어는 술어(predicate)인데 이것은 단순히 참이나 거짓으로 평가되고, AND, OR 같은 연산자로 조합할 수 있다. Specification은 컴포지트 패턴으로 구성되어 있어서 여러 Specification을 조합할 수 있다. 따라서 다양한 검색조건을 조립해서 새로운 검색조건을 쉽게 만들 수 있다.
: 명세 기능을 사용하려면 org.springframework.data.jpa.repository.JpaSpecificationExecutor 인터페이스를 상속받으면 된다.
: Specifications는 명세들을 조립할 수 있도록 도와주는 클래스인데 where(), and(), or(), not() 메소드를 제공한다.
: 명세를 정의하려면 Specification 인터페이스를 구현하면 된다.
: 명세를 정의하려면 Specification 인터페이스를 구현하면 된다. 아래 예제에서는 편의상 내부 무명 클래스를 사용했다. 명세를 정의할 때는 toPredicate(...) 메소드만 구현하면 되는데 JPA Criteria의 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, Mmeber> 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);
}
};
}
}
12.6 사용자 리포지토리 구현
- 스프링 데이터 JPA로 리포지토리를 개발하면 인터페이스만 정의하고 구현체는 만들지 않는다. 하지만 메소드를 직접 구현해야할 때는 사용자 정의 인터페이스를 작성해야 한다. 이때 인터페이스 이름은 자유롭게 지으면 된다. 그리고 사용자 정의 인터페이스를 구현한 클래스를 작성해야 한다. 이때 클래스 이름은 {리포지토리 인터페이스 이름} + Impl로 지어야 한다. 이렇게 하면 스프링 데이터 JPA가 사용자 정의 구현 클래스로 인식한다.
// 사용자 정의 인터페이스
public interface MemberRepositoryCustom {
public List<Member> findMemberCustom();
}
// 사용자 정의 구현 클래스
public class MemberRepositoryImpl implements MemberRespositoryCustom {
@Override
public List<Member> findMemberCustom() {
... // 사용자 정의 구현
}
}
// 사용자 정의 인터페이스 상속
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
}
- 사용자 정의 구현 클래스 이름 끝에 Impl 대신 다른 이름을 붙이고 싶으면 repository-impl-postfix 속성을 변경하면 된다. 참고로 Impl이 기본값이다.
// XML
<respositories base-package="jpabook.jpashop.repository" repository-impl-postfix="Impl" />
// Java config
@EnableJpaRepositories(basePackages = "jpabook.jpashop.repository", repositoryImplementationPostfix = "Impl")
12.7 Web 확장
- 스프링 데이터 프로젝트는 스프링 MVC에서 사용할 수 있는 편리한 기능을 제공한다. 식별자로 도메인 클래스를 바로 바인딩해주는 도메인 클래스 컨버터 기능과 페이징과 정렬 기능을 알아보자.
- 스프링 데이터가 제공하는 Web 확장 기능을 활성화하려면 SpringDataWebConfiguration을 스프링 빈으로 등록하면 된다.
- 아래 설정을 완료하면 도메인 클래스 컨버터와 페이징과 정렬을 위한 HandlerMethodArgumentResolver가 스프링 빈으로 등록된다. 등록되는 도메인 클래스 컨버터는 org.springframework.data.repository.support.DomainClassConverter 이다.
@Configuration
@EnalbeWebMvc
@EnableSpringDataWebSupport
public class WebAppConfig {
...
}
- 도메인 클래스 컨버터 기능
: 도메인 클래스 컨버터는 HTTP 파라미터로 넘어온 엔티티의 아이디로 엔티티 객체를 찾아서 바인딩해준다.
: 도메인 클래스 컨버터는 해당 엔티티와 관련된 리포지토리를 사용해서 엔티티를 찾는다.
@Controller
public class MemberController {
@RequsetMapping("member/memberUpdateFrom")
public String memberUpdateForm(@RequsetParam("id") Member member, Model model) {
model.addAttribute("member", member);
return "member/memberSaveForm";
}
}
도메인 클래스 컨버터를 통해 넘어온 회원 엔티티를 컨트롤러에서 직접 수정해도 실제 데이터베이스에는 반영되지 않는다. 이것은 스프링 데이터와는 관련이 없고 순전히 영속성 컨텍스트의 동작 방식과 관련이 있다.
- OSIV를 사용하지 않으면: 조회한 엔티티는 준영속 상태다. 따라서 변경 감지기능이 동작하지 않는다. 만약 수정한 내용을 데이터베이스에 반영하고 싶으면 병합(merge)을 사용해야 한다.
- OSIV를 사용하면: 조회한 엔티티는 영속 상태다. 하지만 OSIV의 특성상 컨트롤러와 뷰에서는 영속성 컨텍스트를 플러시하지 않는다. 따라서 수정한 내용을 데이터베이스에 반영하지 않는다. 만약 수정한 내용을 데이터베이스에 반영하고 싶으면 트랜잭션을 시작하는 서비스 계층을 호출해야 한다. 해당 서비스 계층이 종료될 때 플러시와 트랜잭션 커밋이 일어나서 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영해 줄 것이다.
- 페이징과 정렬 기능
: 페이징 기능: PageableHandlerMethodArgumentResolver
: 정렬 기능: SortHandlerMethodArgumentResolver
ex) /members?page=0&size=20&sort=name,desc&sort=address.city
: 접두사
@Qualifier 어노테이션 사용, "{접두사명}_"로 구분
public String list(@Qualifier("member") Pageable memberPageable, @Qualifier("order") Pageable orderPageable, ...
ex) /member?member_page=0&order_page=1
: 기본값
Pageable의 기본값은 page=0, size=20이다. 만약 기본값을 변경하고 싶으면 @PageableDefault 어노테이션을 사용하면 된다.
@RequsetMapping(value = "/member_page", method = RequsetMethod.GET)
public String list(@PageableDefault(size = 12, sort = "name", direction = Srot.Direction.DESC) Pageable pageable) {
...
}
12.8 스프링 데이터 JPA가 사용하는 구현체
- 스프링 데이터 JPA가 제공하는 공통 인터페이스는 org.springframework.data.jpa.repository.support.SimpleJpaRepository 클래스가 구현한다.
12.9 JPA 샵에 적용
- 책에 나와있는 예제 코드 참고
12.10 스프링 데이터 JPA와 QueryDSL 통합
- 스프링 데이터 JPA는 2가지 방법으로 QueryDSL을 지원한다.
: org.springframework.data.querydsl.QueryDslPredicateExecutor
: org.springframework.data.querydsl.QueryDslRepositorySupport
- QueryDslPredicateExecutor 사용
- QueryDslRepositorySupport 사용
'JPA > 자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글
Chap.14 컬렉션과 부가 기능 (0) | 2021.08.28 |
---|---|
Chap.13 웹 애플리케이션과 영속성 관리 (0) | 2021.07.30 |
Chap.10 객체지향 쿼리 언어 - 2 (0) | 2021.07.05 |
Chap.10 객체지향 쿼리 언어 - 1 (0) | 2021.06.19 |
Chap.9 값 타입 (0) | 2021.06.19 |