1. 스프링 데이터 JPA 소개

스프링 데이터 JPA는 데이터 접근 개발할 때 구현 클래스 없이 인터페이만 작성해도 개발을 완료할 수 있게 도와준다.

// JPA의 반복적인 CRUD
public class MemberRepository {
    @PersistenceContext
    EntityManager em;
    
    public void save(Member member) {...}
    public Member findOne(Long id) {...}
    public List<Member> findAll() {...}

    public Member findByUsername(String username) {...}
}

public class ItemRepository {
    @Persistence
    EntityManager em;

    public void save(Item item) {...}
    public Member findOne(Long id) {...}
    public List<Member> findAll() {... }

}

스프링 데이터 JPA를 적용하면 위처럼 구현클래스가 필요 없이(실행시점에 생성) 인터페이스만 작성해도 개발 가능

// Spring data 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

2. 스프링 데이터 JPA 설정

// JavaConfig 설정
@Configuration
@EnableJpaRepositories(basePackages = "jpabook.jpashop.repository")
public class appConfig {}
  • backPackages에 있는 레포짓토리 인터페이스를 찾아서 해당 인터페이스를 구현한 클래스를 동적으로 생성한 다음 스프링 빈으로 등록한다.
  • 개발자가 직접 구현 클래스를 만들지 않아도 된다.

3. 공통 인터페이스 기능

// JpaRepository 공통 기능 인터페이스
public interface JpaRepository<T, ID extends Serializable> extends PagingAndSortingRepository<T, ID> {
    ...
}
// JpaRepsository를 사용하는 인터페이스
public interface MemberRepsository extends JpaRepository<Member, Long> {
}
  • 상속받은 JpaRepository<Member, Long> 부분에서 회원 엔티티와 식별자 타입을 지정했다.
  • 이제부터 회원 레포짓토리는 JpaRepository 인터페이스가 제공하는 다양한 기능을 사용할 수 있다.
  • JpaRepository 인터페이스를 상속받으면 추가로 JPA에 특화된 기능을 제공한다.
    • T는 엔티티, ID는 엔티티의 식별자 타입, S는 엔티티와 그 자식 타입을 뜻한다.
    • save(S) : 새로운 엔티티를 저장, 이미 있는 엔티티는 수정
    • delete(T) : 엔티티 하나 삭제. 내부에서 EntityManager.remove() 호출
    • findOne(ID) : 엔티티 하나 조회. 내부에서 EntityManager.find() 호출
    • getOne(ID) : 엔티티를 프록시로 조회. EntityManager.getReference() 호출
    • findAll(…) : 모든 엔티티를 조회. Sort, Pageable 조건을 파라미터로 제공

4. 쿼리 메소드 기능

  • 메소드 이름만으로 쿼리를 생성하는 기능이 있는데 인터페이스에 메소드만 선언하면 해당 메소드의 이름으로 JPQL 쿼리를 생성해서 실행한다.
  • 스프링 데이터 JPA가 제공하는 쿼리 메소드 기능은 크게 3가지가 있다.
    • 메소드 이름으로 쿼리 생성
    • 메소드 이름으로 JPA NamedQuery 호출
    • @Query 어노테이션을 사용해서 레포짓토리 인터페이스에 쿼리 직접 정의

4.1) 메소드 이름으로 쿼리 생성

주의 : 엔티티의 필드명이 변경되면 인터페이스에 정의한 메소드 이름도 꼭 함께 변경해야 함. 그렇지 않으면 애플리케이션 시작 시점에 오류 발생.

4.2) JPA NamedQuery

  • 이름 그대로 쿼리에 이름을 부여해서 사용하는 방법
    // @NamedQuery 어노테이션으로 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<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);
}
  • spring data jpa는 선언한 “도메인클래스 + . + 메소드이름”으로 NamedQuery를 찾아서 실행한다. 만약 실행할 NamedQuery가 없으면 메소드 이름으로 쿼리 생성 전략을 사용한다.
  • @Param : 이름기반 파라미터 바인딩할 때 사용하는 어노테이션ㅇ다.

4.3) @Query, 리포지토리 메소드에 쿼리 정의

// 책 p549 애플리케이션 실행 시점에 문법 오류를 발견할 수 있는 장점..? 단 아닌지 ?

// 메소드에 JPQL 쿼리 작성
public interface MemberRepository extends JpaRepository<Member, Long> {
    @Query("select m from Member m where m.username = ?1")
    Member findByUsername(String username);
}
  • 네이티브 SQL을 사용하려면 @Query 어노테이션에 nativeQuery = true를 설정한다.
  • 스프링 데이터 JPA가 지원하는 파라미터 바인딩을 사용하면 JPQL은 위치 기반 파라미터를 1부터 시작하지만 네이티브 SQL은 0부터 시작한다.

4.4) 파라미터 바인딩

스프링 데이터 JPA는 위치 기반 파라미터 바인딩과 이름 기반 파라미터 바인딩을 지원한다.

select m from Member m where m.username = ?1  // 위치기반
select m from Member m where m.username = :name  // 이름기
  • 기본값은 위치 기반인데 파라미터 순서로 바인딩한다.
  • 이름기반 파라미터를 사용하려면 @Param 어노테이션을 사용하면 된다.

4.5) 벌크성 수정 쿼리

// JPA를 사용한 벌크성 수정 쿼리
int bulkPriceUp(String stockAmount) {
    ...
    String qlString = "update Product p set p.price = p.price * 1.1 where p.stockAmount < :stockAmount";
    
    int resultCount = em.createQuery(qlString)
        .setParamter("stockAmount", stockAmount)
        .executeUpdate();
}
// 스프링 데이터 JPA를 사용한 벌크성 수정 쿼리
@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로 설정하면 된다.

4.6) 반환 타입

  • 결과가 한건 이상이면 컬렉션 인터페이스를 사용하고, 단건이면 반혼 타입을 지정한다.
  • 조회 결과가 없으면 컬렉션은 빈 컬렉션을 반환하고, 단건은 null을 반환한다.
  • 단건을 기대하고 반환 타입을 지정했는데 결과가 2건 이상이면 NonUniqueResultException 예외가 발생한다.
  • 단건으로 지정한 메소드를 호출했는데 결과가 없으면 NoResultException 예외가 발생하는데 스프링 데이터 JPA는 단건을 조회 할 때 이 예외가 발생하면 예외를 무시하고 대신 null을 반환한다.
    (참고: Optional)

4.7) 페이징과 정렬

  • 스프링 데이터 JPA는 쿼리 메소드에 페이징과 정렬 기능을 사용할 수 있도록 2가지 파라미터를 제공한다.
    • 정렬기능
    • 페이징기능
      • 파라미터에 Pageable를 사용하면 반환 타입으로 List나 Page를 사용할 수 있다.
      • 반환타입으로 Page를 사용하면 전체 데이터 건수를 조회하는 count 쿼리를 추가로 호출한다.
// count 쿼리 사용
Page<Member> findByName(String name, Pageable pageable);


// count 쿼리 사용 안함
List<Member> findByName(String name, Pageable pageable);

List<Member> findByName(String name, Sort sort);
// Page 사용 예제 실행 코드
// 페이징 조건과 정렬 조건 설정
PageRequest pageRequest = new PageRequest(0, 10, new Sort(Direction.DESC, "name"));
Page<Member> result = memberRepositry.findByStartingWith("김", pageRequery);

List<Member> members = result.getContent();	// 조회된 데이터
int totalPages = result.getTotalPages();	// 전체 페이지 수
boolean hasNextPage = result.hasNextPage();	// 다음 페이지 존재 여부
  • Pageable은 인터페이스여서 실제 사용할 때는 인터페이스를 구현한 PageRequest 객체를 사용한다.
  • PageRequest 생성자의 첫 번째 파라미터에는 현재페이지, 두 번째 파라미터에는 조회할 데이터 수 여기에 추가로 정렬 정보도 파라미터로 넣을 수 있다.
  • 페이지는 0부터 시작한다.

지금까지 설명한 Pageable, Page를 사용하면 지루하고 반복적인 페이징 처리를 손쉽게 개발할 수 있다.

주의
PageRequest의 생성자는 deprecated되어 추천하지 않음. 대신에, PageRequest.of(page, size) 를 사용할 것.

4.8) 힌트

  • JPA 쿼리 힌트는 SQL 힌트가 아니라 JPA 구현체에게 제공하는 힌트다.
  • forCounting 속성은 반환 타입으로 Page 인터페이스를 적하면 추가로 호출 하는 페이징을 위한 count 쿼리에도 힌트를 적용할지를 설정하는 옵션이다. (기본값 true)
    @QueryHints(value = {@QueryHint(name="org.hibernate.readOnly", value="true")}, forCounting=true)
    Page<Member> findByName(String name, Pageable pageable);
    

4.9) Lock

  • 쿼리 시 락을 걸려면 @Lock 어노테이션을 사용하면 된다.
    @Lock(LockModeType.PESSIMITIC_WRITE)
    List<Member> findByName(String name);
    

5. 명세

도메인 주도 설계에서 명세라는 개념을 소개하는데, 스프링 데이터 JPA 는 JPA Criteria 로 이 개념을 사용할 수 있음. 명세를 이해하기 위한 핵심 단어는 술어(Predicate)
스프링 데이터 JPA 는 이 술어를 org.springframework.data.jpa.domain.Specification 클래스로 정의함.
Specification 은 컴포지트 패턴으로 구성되어 있어서 여러 Specification 으로 조합할 수 있다. 즉, 다양한 검색 조건을 조립해서 새로운 검색 조건을 쉽게 만들 수 있음

// JpaSpecificationExecutor 상속
pubilc interface OrderRepsitroy extends JpaRepository<Order, Long>, JpaSpecificationExecutor<Order>{
}

// JpaSpecificationExecutor 인터페이스
public interface JpaSpecificationExecutor<T> {
  T findOne (Specification<T> spec);
  ...
}

// 명세 사용 코드
// Specification 은 명세들을 조립할 수 있도록 도와주는 클래스인데, 
// where(), and(), or(), not() 메소드를 제공
public List<Order> findOrders (String name) {
  List<Order> result = orderRepository.findAll(
  				where(memberName(name)).and(isOrderStatus())
  );
  return result;
}

6. 사용자 정의 리포지토리 구현

  • 스프링 데이터 JPA는 필요한 메소드만 구현할 수 있는 방법을 제공한다.
  • 먼저 직접 구현할 메소드를 위한 사용자 정의 인터페이스를 작성한다.
// 사용자 정의 인터페이스
public interface MemberRepositoryCustom{
    public List<Member> findMemberCustom();
}
  • 다음으로 사용자 정의 인터페이스를 구현한 클래스를 작성한다.
  • 이때 클래스 이름을 짓는 규칙이 있는데 레포짓토리 인터페이스 이름 + Impl로 지어야 한다.
  • 이렇게 하면 스프링 데이터 JPA가 사용자 정의 구현 클래스로 인식한다.
    // 사용자 정의 구현 클래스
    public class MemberReposityImpl implements MemberRepositoryCustom {
      @Override
      public List<Member> findMemberCustom() {
          // 사용자 정의 구현
      }
    }
    
  • 마지막으로 레포짓토리 인터페이스에서 사용자 정의 인터페이스를 상속 받으면 된다.
// 사용자 정의 인터페이스 상속
public interface MemberRepository extends JpaRepository<Member, Long>, MemberRepositoryCustom {
    
}
  • 만약 사용자 정의 구현 클래스 이름 끝에 Impl 대신 다른 이름을 붙이고 싶으면 repository-impl-postfix 속성을 변경하면 된다.
@EnableJpaRepositories(basePackages = "jpabook.jpashop.repository", repositoryImplementationPostfix="Impl")

7. Web 확장

7.1) 설정

  • 스프링 데이터가 제공하는 Web 확장 기능을 활성화하려면 SpringDataWebConfiguration을 스프링 빈으로 등록하면 된다.
  • java config를 사용하면 @EnableSpringDataWebSupport 어노테이션을 사용하면 된다.

7.2) 도메인 클래스 컨버터 기능

  • 도메인 클래스 컨버터는 HTTP 파라미터로 넘어온 엔티티의 아이디로 엔티티 객체를 찾아서 바인딩해준다.
// 회원의 아이디로 회원 엔티티 조회
@Controller
public class MemberController {
    @AutoWried MemberRepository memberRepositry;
    
    @RequestMapping("member/memberUpdateForm")
    public String memberUpdateForm(@Request("id") Long id, Model model) {
        Member member = memberRepository.findOne(id);
        model.addAttribute("member", member);
        return "member/memberSaveForm";
    }
}
// 도메인 클래스 컨버터 적용
@Controller
public class MemberController {
    @AutoWried MemberRepository memberRepositry;
    
    @RequestMapping("member/memberUpdateForm")
    public String memberUpdateForm(@Request("id") Member member, Model model) {
        model.addAttribute("member", member);
        return "member/memberSaveForm";
    }
}
  • 도메인 클래스 컨버터가 중간에 동작해서 아이디를 회원 엔티티 객체로 변환해서 넘겨준다.
  • 도메인 클래스 컨버터는 해당 엔티티와 관련된 레포지토리를 사용해서 엔티티를 찾는다. 도메인 클래스 컨버터를 통해 넘어온 회원 엔티티를 컨트롤러에서 직접 수정해도 실제 데이터 베이스에는 반영되지 않는다.

OSIV를 사용하지 않으면 : 조회한 엔티티는 준영속 상태이다. 따라서 변경 감지기능이 동작하지 않는다. 만약 수정한 내용을 데이터베이스에 반영하고 싶으면 병합(merge)를 사용해야 한다.

OSIV를 사용하면 : 조회한 엔티티는 영속 상태다. 하지만 OSIV의 특성상 컨트롤러와 뷰에서는 영속성 컨텍스트를 플러시하지 않는다. 따라서 수정한 내용을 데이터베이스에 반영하지 않는다. 만약 수정한 내용을 데이터베이스에 반영하고 싶음녀 트랜잭션을 시작하는 서비스 계층을 호출해야 한다. 해당 서비스 계층이 종료될 때 플러시와 트랜잭션 커밋이 일어나서 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영해 줄 것이다.

7.3) 페이징과 정렬 기능

  • 스프링 데이터가 제공하는 페이징과 정렬 기능을 스프링 MVC에서 사용할 수 있도록 HandlerMethodArgumentResolver를 제공한다.
    • 페이징 기능 : 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 : 정렬 조건을 정의한다. 예) 정렬속성, 정렬방향을 변경하고 싶으면 sort파라미터를 추가하면 된다.
      • 예) /members?page=0&size=20&sort=name,desc&sort=address.city
      • 페이지를 1부터 시작하고 싶으면 PageableHandlerMethodArgumentResolver를 스프링 빈으로 직접 등록하고 setOneIndexedParameters를 true로 설정하면 된다.

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

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

@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID extends Serializable> implements JpaRepository<T, ID>, JpaSpecificationExecutor<T> {
  ...
}
  • @Repository
    JPA 예외를 스프링이 추상화한 예외로 변환한다.?
    링크 : JPA예외

9. JPA 샵에 적용

지금까지 했던 과정 재설명(책 참고)

10. 스프링 데이터 JPA와 QueryDSL 통합

10.1) QueryDslPredicateExecutor 사용

  • 다음처럼 리포짓토리에서 QueryDslPredicateExecutor를 상속받으면 된다.
    public interfavce ItemRepository extends JpaRepository<Item, Long>, QueryDslPredicateExecutor<Item> {}
    
// QueryDSL 사용 예제
QItem item = QItem.item;
Iterable<Item> result = itemRepository.findAll(item.name.contains("장난감")
                                      .and(item.price.between(1000,2000)));
  • QueryDslPredicateExecutor 인터페이스를 보면 QueryDSL을 검색조건으로 사용하면서 스프링 데이터 JPA가 제공하는 페이징 정렬 기능도 함
// QueryDslPredicateExecutor 인터페이스
public interface QueryDslPredicateExecutor<T> {
    T findOne(Predicate predicate);
    Iterable<T> findAll(Predicate predicate);
    Iterable<T> findAll(Predicate predicate, OrderSpecifier<?>... order);
    Page<T> findAll(Predicate predicate, Pageable pageable);
    long count(Predicate predicate);
}
  • QueryDslPredicateExecutor는 스프링 데이터 JPA에서 편리하게 QueryDSL을 사용할 수 있지만 기능에 한계가 있는데 join, fetch를 사용할 수가 없다. (JPQL에서 이야기하는 묵시적 조인은 가능)
  • QueryDSL이 제공하는 다양한 기능을 사용하려면 JPAQuery를 직접 사용하거나 스프링 데이터 JPA가 제공하는 QueryDslRepositorySupport를 사용해야 한다.

10.2) QueryDslRepositorySupport 사용

  • QueryDSL의 모든 기능을 사용하려면 JPAQuery 객체를 직접 생성해서 사용하면 된다.
  • QueryDslRepositorySupport를 상속 받아 사용하면 조금 더 편리하게 QueryDSL를 사용할 수 있다.
    // CustomOrderRepository 사용자 정의 레파짓토리
    public interface CustomOrderRepository {
      public List<Order> search(OrderSearch orderSearch);
    }
    
  • 스프링 데이터 JPA가 제공하는 공통 인터페이스를 직접 구현할 수가 없기 때문에 CustomOrderRepository라는 사용자 정의 레포짓토리를 만들었다.
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;
        
        JPAQuery 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);
    }
}
  • QueryDslRepositorySupport를 사용해서 QueryDSL로 구현한 예제인데 검색 조건에 따라 동적으로 쿼리를 생성한다.
  • 참고로 QueryDslRepositorySupport에 엔티티 클래스 정보를 넘겨주어야 한다.
// QueryDslRepositorySupport 코드
@Repository
public abstract class QueryDslRepositorySupport {
    // 엔티티 매니저 반환
    protected EntityManager getEntityManager(){
        return EntityManager;
    }
    
    // from절 반환
    protected JPAQuery from(EntityPath<?>... paths) {
        return querydsl.createQuery(paths);
    }
    
    // QueryDSl delete 절 반환
    proected DeleteClause<JPADeleteClause> delete(EntityPath<?> path) {
       return new JPADeleteClause(entityManager, path);
    }
    
    // QueryDSL update 절 반환
    protected UpdateClause<JPAUpdateClause> update(EntityPath<?> path) {
        return new JPAUpdateClause(entityManger, path);
    }
    
    // 스프링 데이터 JPA가 제공하는 Querydsl를 편하게 사용하도록 돕는 헬퍼 객체 반환
    proected QueryDsl getQueryDsl() {
        return this.querydsl;
    }
}

댓글남기기