12장-Spring Data Jpa

자바 ORM 표준 JPA 프로그래밍 12장을 요약한 내용 입니다.

스프링 데이터 JPA

대부분의 데이터 접근 계층은 일명(CRUD)로 불리는 유사한 등록, 수정, 삭제, 조회 코드를 반복해서 개발해야 한다. JPA를 사용해서 개발할때도 이와 같은 문제가 반복 된다

public class MemberRepository {
  @PersistentContext
  EntityManager em;
  public void save(Member member) { ... }
  public Member findOne(Long id) { ... }
  public List<Member> findAll() { ... }
}

public class ItemRepository {
  @PersistentContext
  EntityManager em;
  public void save(Item item) { ... }
  public Item findOne(Long id) { ... }
  public List<Item> findAll() { ... }
	public Member findByUsername(String username) { ... }
}

spring-data-jpa는 스프링 프레임워크에서 jpa를 편리하게 사용할 수 있도록 지원하는 프로젝트다.

이 프로젝트는 데이터 접근 계층을 개발할 때 지루하게 반복되는 crud 문제를 해결했다.

CRUD처리를 위해 공통된 인터페이스를 제공한다.

그리고 레포지토리를 개발할 때 인터페이스만 작성하면 실행 시점에 jpa가 동적으로 구현 객체를 생성해 주입해준다.

따라서 데이터 접근 계층을 개발할 대 구현 클래스 없이 인터페이스만 작성해도 개발을 완료할 수 있다

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

public interface ItemRepository extends JpaRepository<Item, Long> {}

https://user-images.githubusercontent.com/19490925/84344727-83c86e80-abe6-11ea-9d6f-54cb7b7e1bcd.png

일반적인 Curd기능은 jpaRepository 인터페이스가 공통으로 제공하므로 문제가 없다.

하지만 findByUsername처럼 직접 작성한 메서드는 어떻게 해야 될까?

놀랍게도 JPA는 메소드 이름을 분석해 다음 JPQL을 실행한다.

select m from Member m where username =:username

스프링 데이터 프로젝트

spring-data-jpa는 스프링 데이터 프로젝트의 하위 프로젝트 중 하나다.

https://user-images.githubusercontent.com/19490925/84345075-592ae580-abe7-11ea-8837-a7ccc7c6c1d6.png

스프링 데이터spring Data 프로젝트는 JPA, 몽고DB, REDIS, HADOOP 같은 다양한 데이터 저장소에 대한 접근을 추상화해서 개발자 편의를 제공하고 지루하게 반복하는 데이터 접근 코드를 줄여준다.

여기서 스프링 데이터 JPA프로젝트는 JPA에 특화된 기능을 제공한다. 스프링 프레임워크와 JPA를 함께 사용한다면 스프링 데이터 JPA의 사용을 적극 추천한다.

스프링 데이터 JPA 설정

공통 인터페이스 기능

스프링 데이터 JPA는 간단한 CRUD 기능을 공통으로 처리하는JpaRepository 인터페이스를 제공한다. 스프링 데이터 JPA를 사용하는 가장 단 순한 방법은 이 인터페이스를 상속받는 것이다. 그리고 제네릭에 엔티티 클래스와 엔티티 클래스가 사용하는 식별자 타입을 지정하면 된다.

public interface JpaRepository<T, Id extends Serializable> extends PagingAndSortingRepository<T, ID> {
  ...
}

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

JpaRepository의 인터페이스 구조를 살펴보자.

https://user-images.githubusercontent.com/19490925/84346166-5382cf00-abea-11ea-8140-96f92ab674fd.png

위 그림을 보면 윗부분에 스프링 데이터 모듈이 있고 그 안에 Repository, CrudRepository, PagingAndSortingRepositry가 있는데 이것은 스프링 데이터 프로젝트가 공통으로 사용하는 인터페이스다.

스프링 데이터 JPA가 제공하는 JpaRepository인터페이스는 여기에 추가로 JPA에 특화된 기능을 제공한다.

참고로 T는 엔티티, ID는 엔티티의 식별자 타입, S는 엔티 티와 그 자식 타입을 뜻한다.

주요 메소드

  • save(S) : 새로운 엔티티는 저장하고 이미 있는 엔티티는 수정한다.
  • delete(T): 엔티티 하나를 삭제한다. 내부에서 EntityManager.remove()를 호출한다
  • findOne(ID) : 엔티티 하나를 조회한다. 내부에서 EntityManager.find()를 호출 한다.
  • getOne(ID): 엔티티를 프록시로 조회한다. 내부에서 EntityManager.getReference()를 호출한다.
  • findAll(): 모든 엔티티를 조회한다. 정렬(sort)이나 페이지(pageable) 조건을 파라미터로 제공할 수 있다.

getOne vs findOne

save(S) 메소드는 엔티티에 식별자 값이 없으면(null이면) 새로운 엔티티로 판단해서 EntityManager.persist()를 호출하고 식별자 값이 있으면 이미 있는 엔티티로 판단해서 EntityManager.merge() 를 호출한다. 필요하다면 스프링 데이터 JPA의 기능을 확장해서 신규 엔티티 판단 전략을 변경할 수 있다. JpaRepository 공통 인터페이스를 사용하면 일반적인 CRUD를 해결할 수 있다.

쿼리 메소드 기능

메소드 이름만으로 쿼리를 생성하는 기능

  • 메소드 이름으로 쿼리 생성
  • 메소드 이름으로 JPA NamedQuery 호출
  • @Query 어노테이션을 사용해서 리포지토리 인터페이스에 쿼리 직접 정의

메소드 이름으로 쿼리 생성

public interface MemberRepository extends Repository<Member, Long> {
  List<Member> findByEmailAndName(String email, String name;
}

인터페이스에 정의한 findByEmailAndName() 메소드를 실행하면 스프링 데이터 JPA는 메소드 이름을 분석해서 JPQL을 생성하고 실행한다. 실행된 JPQL은 다음과 같다.

select m from Mernber m where m.email = ?1 and m.name = ?2

물론 정해진 규칙에 따라서 메소드 이름을 지어야 한다

링크

이 규칙을 어기고 작성할 경우 애플리케이션 구동시 에러가 발생한다.

JPA NamedQuery

10장에서 배웠던 NamedQuery역시 실행할 수 있다.

public interface MemberRepository extends Repository<Member, Long> {
  List<Member> findByUsername(@Param("username") String username);
}

spring-data-jpa는 선언한 “도메인 클래스 + .(점) + 메소드 이름” 으로 NamedQuery를 찾고, 없는 경우에는 메소드 이름으로 쿼리 생성 전략을 사용한다. (필요시 전략은 변경 가능하다)

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

public interface MemberRepository extends Repository<Member, Long> {
  @Query("select m from Member m where m.username = ?1") 
	Member findByUsername(String username);
}

JPQL이 아닌 Native SQL역시 사용 가능하다. 차이점이라면 파라미터의 인덱스가 0부터 시작한다는 점이다.

public interface MemberRepository extends Repository<Member, Long> {
  @Query(value = "SELECT * FROM MEMBER WHERE USERNAME = ?0", nativeQuery = true)
  Member findByUsername(String username);
}

파라미터 바인딩

위치기반 바인딩과 이름 기반 바인딩 둘 모두를 지원한다.

select m from Member m where m.username = ?1 → 위치 기반

select m from Member m where m.username = :name → 이름 기반

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

벌크성 수정 쿼리

@Modifying
@Query("update Product p set p.price = p.price * 1.1 where p.stockAmount : stockAmount")
int bulkPriceUp(@Param("stockAmount") String stockAmount));

사용한뒤 바로 영속성 컨텍스트를 초기화하고 싶다면 clearAutomatically옵션을 true로 설정하면 된다(기본 값은 false)

반환 타입

스프링 데이터 JPA는 유연한 반환 타입을 지원하는데, 결과가 한 건 이상이면 컬렉션, 단건이면 반환 타입을 지정한다

List findByName(String name); → 컬렉션

Member findByEmail(String email); → 단건

조회 결과가 없다면 단건은 null, 컬렉션은 빈 컬렉션을 반환한다

페이징과 정렬

  • org.springframework.data.domain.Sort - 정렬 기능
  • org.springframework.data.doamin.Pageable - 페이징 기능
// count 쿼리 사용
Page<Member> findByName(String name, Pageable pageab1e);
// count 쿼리 미사용
List<Member> findByname(String name, Pageable pageab1e);
  • 검색 조건: 이름이 김으로 시작하는 회원
  • 정렬 조건: 이름으로 내림차순
  • 페이징 조건: 첫번째 페이지, 페이지당 보여줄 데이터는 10건
public interface MemberRepository extends Repository<Member, Long> {
  Page<Member> findByNameStartWith(String name, Pageable pageab1e);
}
// 조건 설정
PageRequest pageRequest = new PageRequest(0, 10, new Sort(Direction.DESC, "name"));
Page<Member> result = memberRepository.findByNameStartWith("김", pageRequest);
List<Member> members = result.getContent(); // 조회된 실제 데이터 (10개)
int totalPages = result.getTotalPages(); // 전체페이지 수
boolean hasNextPage = result.hasNextPage(); // 다음 페이지 존재여부

https://user-images.githubusercontent.com/19490925/84351207-d5c4c080-abf5-11ea-8ed6-c5b8d6ea5e50.png

힌트

SQL 힌트가 아닌 JPA 구현체에게 제공하는 힌트

@QueryHints(value = {@QuryHint(name="org.hibernate.readonly", value= "true")}, forCounting = true)

Lock

쿼리 시 락을 걸고 싶다면 org.springframework.data.jpa.repository.Lock Annotation을 걸면 된다.

명세

DDD 책에서 명세(Specification)이라는 개념을 소개하는데, JPA 도 JPA Criteria로 이 개념을 사용할 수 있다.

명세를 이해하기위한 핵심단어는 술어(predicate)인데, 이것은 단순히 참이나 거짓으로 평가된다.

예를 들어 데이터를 검색하기 위한 제약 조건 하나하나를 술어라 할 수 있다. 이 술어를 스프링 데이터 jpa 클래스로 정의했다.

사용하기 위해서는 org.springframework.data.jpa.repository.JpaSpecificationExecutor 인터페이스를 상속 받으면 된다.

// 선언
public interface OrderRepository extends JpaRepository<Order, Long>, JpaSpecificationExecutor<Order> {}
// 인터페이스
public interface JpaSpecificationExecutor<T> {
    T findOne(Specification<T> spec);
    List<T> findAll(Specification<T> spec);
    ...
}
// 사용

import static org.springframework.data.jpa.domain.Specification.*;

List<Order> result = orderRepository.findAll(where(memberName(name).and(isOrderStatus())));

이를 사용하려면 memberNameisOrderStatus를 정의해야 한다. 이는 OrderSpec안에 정의되어 있어야 한다. Specification을 정의하려면 Specification인터페이스를 구현하는 클래스(OrderSpec)에서 toPredicate메소드를 구현하면 된다.

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

public interface MemberRepositoryCustom {
    public List<Member> findMemberCustom();
}

/**    사용자 정의 레포지토리 네이밍 규칙은 인터페이스이름 + Impl이다 */
public class MemberRepositoryImpl implements MemberRepositoryCustom {
    @Override
    public List<Member> findMemberCustom() {
        ... // 사용자 정의 구현
    }
}
// 실 사용
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {}

Web 확장

스프링 데이터 프로젝트는 스프링 MVC에서 사용할 수 있는 편리한 기능을 제공한다. 식별자로 도메인 클래스를 바로 바인딩해주는 도메인 클래스 컨버터 기능과, 페이징과 정렬 기능을 알아보자

설정

@Configuration
@EnableWebMvc
@EnableSpringDataWebSupportpublic class Config {
    ...
}

설정 완료시 HandlerMethodArgumentResolver가 등록된다.

도메인 클래스 컨버터 기능

@Controller
public class MemberConroller {
    @Autowired MemberRepository memberRepository;
    @RequestMapping("member/memberUpdateForm")
    public String memberUpdateForm(@RequestParam("id") Long id, Model model) {
        Member member = memberRepository.findOne(id); // 맴버를 조회
    }
}

@Controller
public class MemberConroller {
    @Autowired MemberRepository memberRepository;
    @RequestMapping("member/memberUpdateForm")
    public String memberUpdateForm(@RequestParam("id") Member member, Model model) {
                // Member가 엔티티로 내려옴    
}}

주의할것

도메인 클래스 컨버터를 통해 넘어온 회원 엔티티를 컨트롤러에서 직접 수정해도 실제 데이터 베이스에는 반영되지 않는다. 참고로 이것은 스프링 데이터와는 관련이 없고 순전히 영속성 건 텍스트의 동작 방식과 관련이 있다. 웹 애플리케이션에서 영속성 컨텍스트의 동작 방식과 OSIV 에 관한 자세한 내용은 13장에서 설명한다.

페이징과 정렬 기능

  • 페이징 기능: PageableHandlerMethodArgumentResolver
  • 정렬 기능: SortHandlerMethodArgumentResolver
@RequestMapping(value = "")
public String list(Pageable pageab1e) {
    Page<Member> page = memberService.findMembers(pageab1e);
}

요청 파라미터

  • page: 현재 페이지, 0부터 시작
  • size: 한 페이지에 노출할 데이터 건수
  • sort: 정렬조건(ASC, DESC)

스프링 데이터 JPA가 사용하는 구현체

스프링 데이터 JPA가 제공하는 공통 인터페이스는 org.springframework.data.jpa.repository.support.SimpleJpaRepository 클래스가 구현한다

  • @Repository 적용: Jpa예외를 스프링이 추상화한 예외로 반환한다
  • @Transactional 트랜잭션 적용: JPA의 모든 변경은 트랜잭션 안에서 이루어져 야 한다. 스프링 데이터 JPA가 제공하는 공통 인터페이스를 사용하면 데이터를 변경 (등록, 수정, 삭제)하는 메소드에 @Transactional로 트랜잭션 처리가 되어 있다. 따라서 서비스 계층에서 트랜잭션을 시작하지 않으면 리포지토리에서 트랜잭션을 시작한다. 물론 서비스 계층에서 트랜잭션을 시작했으면 리포지토리도 해당 트랜잭션을 전파받아서 그대로 사용한다.
  • @Transactiona1 (readOnly = true) : 데이터를 조회하는 메소드에는 readOnly = true 옵션이 적용되어 있다. 데이터를 변경하지 않는 트랜책션에서 readOnly = true 옵션을 사용하면 플러시를 생략해서 약간의 성능 향상을 얻을 수 있음.
  • save() 메소드: 이 메소드는 저장할 엔티티가 새로운 엔티티면 저장(persist)하고 이미 있는 엔티티면 병합(merge)한다. 새로운 엔티티를 판단하는 기본 전략은 엔티티의 식별자로 판단하는데 식별자가 객체일 때 null, 자바 기본 타입일 때 숫자 0 값이면 새로운 엔티티로 판단한다.