SpringDataJPA에 대해 - 기본 기능

쿠우·2023년 1월 11일
0

공통 인터페이스 기능

  • 아래의 내용은 순수 JPA와 Spring data를 사용했을 때 차이점을 비교하면서 알아보았다.

- 순수 JPA 기반 repository

예제 MemberRepository

@Repository
public class MemberJpaRepository {

    @PersistenceContext
    private EntityManager em;

    public Member save(Member member) {
        em.persist(member);
        return member;
    }

    public void delete(Member member) {
        em.remove(member);
    }

    public List<Member> findAll() {
        return em.createQuery("select m from Member m", Member.class)
                .getResultList();
    }

    public Optional<Member> findById(Long id) {
        Member member = em.find(Member.class, id);
        return Optional.ofNullable(member);
    }

    public long count() {
        return em.createQuery("select count(m) from Member m", Long.class)
                .getSingleResult();
    }

    public Member find(Long id) {
        return em.find(Member.class, id);
    }
}

예제 TeamRepository

@Repository
public class TeamJpaRepository {
    @PersistenceContext
    private EntityManager em;

    public Team save(Team team) {
        em.persist(team);
        return team;
    }
    
    public void delete(Team team) {
        em.remove(team);
    }

    public List<Team> findAll() {
        return em.createQuery("select t from Team t”, Team.class)
                .getResultList();
    }

    public Optional<Team> findById(Long id) {
        Team team = em.find(Team.class, id);
        return Optional.ofNullable(team);
    }

    public long count() {
        return em.createQuery("select count(t) from Team t”, Long.class)
                .getSingleResult();
    }
}
  • 순수 JPA를 통한 repository를 통해 말하고 싶은 것은 코드가 길게 쭉 있다는 부분이다.
  • Spring data를 통해 어떻게 변하는지 확인해보자

- Spring Data 의 공통 인터페이스

  • 우리는 spring data의 공통인터페이스 라는 것을 통하여 편안함을 얻을 것이다.

1) 공통 인터페이스 설정을 해야한다. (boot는 안해두 된다.)

  • JavaConfig 설정(스프링 부트 사용시 생략 가능하다)
  • 스프링 부트 사용시 @SpringBootApplication 위치를 기본으로 지정한다(해당 패키지와 하위 패키지 인식)
    (만약 위치가 달라지면 @EnableJpaRepositories 필요)
@Configuration
@EnableJpaRepositories(basePackages = "jpabook.jpashop.repository")
public class AppConfig {}

2) 공통 인터페이스의 효과는 무엇일까?

  • 스프링 데이터 JPA가 구현 클래스를 대신 생성한다.
  • org.springframework.data.repository.Repository 를 구현한 클래스는 스캔 대상이다.
    (인터페이스를 동작시켜서 출력해보면 class com.sun.proxy.$ProxyXXX 프록시가 생성됨을 알 수 있음)
  • 컴포넌트 스캔을 스프링 데이터 JPA가 자동으로 처리해서 @Repository 애노테이션 생략 가능하다.
    (JPA 예외를 스프링 예외로 변환하는 과정도 자동으로 처리)

3) 공통 인터페이스 적용 방법

  • 순수 JPA 코드로 구현하는 것이 아닌 스프링 데이터 JPA가 제공하는 공통 인터페이스를 사용하여 repository 생성
  • JpaRepository<엔티티 타입, 식별자 타입>
//  스프링 데이터 JPA가 구현클래스 대신 생성
public interface MemberRepository extends JpaRepository<Member, Long> {
}
import org.springframework.data.jpa.repository.JpaRepository;
import study.datajpa.entity.Team;

public interface TeamRepository extends JpaRepository<Team, Long> {
}

4) 공통 인터페이스는 어떤 구조로 되어있을까?

  • 스프링 데이터의 공통 부분에서 각 데이터에 맞게 세분화 되어있다.
    (라이브러리가 나눠져 있음)

  • 그리고 보편적으로 사용한다 싶은 기능들은 다 메서드로 정의 되어있다.
데이터 공통라이브러리는 보통 DB에서 사용한다 싶은 기능들을,
세분화된 데이터 JPA는 JPA에 맞춘 기능들을 정의했다.

주요 메서드
save(S) : 새로운 엔티티는 저장하고 이미 있는 엔티티는 병합한다.
delete(T) : 엔티티 하나를 삭제한다. 내부에서 EntityManager.remove() 호출
findById(ID) : 엔티티 하나를 조회한다. 내부에서 EntityManager.find() 호출
getOne(ID) : 엔티티를 프록시로 조회한다. 내부에서 EntityManager.getReference() 호출
findAll(…) : 모든 엔티티를 조회한다. 정렬( Sort )이나 페이징( Pageable ) 조건을 파라미터로 제공할 수 있다

쿼리 메소드 기능

쿼리 메소드 기능이란?

  • 스프링 데이터 JPA가 주요 메서드 뿐만 아니라 어떤 조건을 요구하는 기능까지 구현해준다.
    ( 마법 같다! )

쿼리 메소드의 기능 3가지!

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

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

  • 메소드 이름을 분석해서 JPQL 쿼리 실행한다
  • 짤막짤막한 쿼리들을 하나하나 치다보면 생산성이 낮아진다. 이럴 때 쓰면 좋다.
  • 복잡해지면 이 방법보다 다른 방법으로 푸는 것이 좋다.
    (조건이 길어지면 메소드명이 한 없이 길어지다보니 보기가 안좋다.)

이름과 나이를 기준으로 회원을 조회하려면?

  • 순수 JPA repository
public List<Member> findByUsernameAndAgeGreaterThan(String username, int age) {
    return em.createQuery("select m from Member m where m.username = :username
            and m.age > :age")
            .setParameter("username", username)
            .setParameter("age", age)
            .getResultList();
}
  • 스프링 데이터 JPA
    (스프링 데이터 JPA는 메소드 이름을 분석해서 JPQL을 생성하고 실행한다.)
public interface MemberRepository extends JpaRepository<Member, Long> {
     List<Member> findByUsernameAndAgeGreaterThan(String username, int age);
}

쿼리 메소드 필터 조건
스프링 데이터 JPA 공식 문서 참고: (https://docs.spring.io/spring-data/jpa/docs/current/
reference/html/#jpa.query-methods.query-creation)

스프링 데이터 JPA가 제공하는 쿼리 메소드 기능

Tip. 이 기능은 엔티티의 필드명이 변경되면 인터페이스에 정의한 메서드 이름도 꼭 함께 변경해야 한다.
그렇지 않으면 애플리케이션을 시작하는 시점에 오류가 발생한다.
이렇게 애플리케이션 로딩 시점에 오류를 인지할 수 있는 것이 스프링 데이터 JPA의 매우 큰 장점이다.

2) JPA NamedQuery

  • 일단 이 기능을 실무에서 거의 사용하지 않는다. (귀찮음)
  • 하나의 장점이 있다면 애플리케이션이 로딩될 때 파싱해서 에러를 잡아준다.
  • @NamedQuery 어노테이션으로 Named 쿼리 정의

@Entity
@NamedQuery(
        name="Member.findByUsername",
        query="select m from Member m where m.username = :username")
public class Member {
 ...
}
  • 스프링 데이터 JPA로 NamedQuery 사용
@Query(name = "Member.findByUsername")
List<Member> findByUsername(@Param("username") String username);
  • @Query 를 생략하고 메서드 이름만으로 Named 쿼리를 호출할 수 있다.
 이유는? 
스프링 데이터 JPA는 선언한 "도메인 클래스 + .(점) + 메서드 이름"으로 Named 쿼리를 찾아서 실행
만약 실행할 Named 쿼리가 없으면 메서드 이름으로 쿼리 생성 전략을 사용한다.
즉, 전략상 Named쿼리 찾는게 우선된다.
전략은 바꿀 수 있지만 공식적으로 권장하지 않는다.

3) @Query, 리포지토리 메소드에 쿼리 정의하기

  • 주로 사용한다. 강려크하다.
public interface MemberRepository extends JpaRepository<Member, Long> {

    @Query("select m from Member m where m.username= :username and m.age = :age")
    List<Member> findUser(@Param("username") String username, @Param("age") int age);
}
  • @org.springframework.data.jpa.repository.Query 어노테이션을 사용한다.
  • 실행할 메서드에 정적 쿼리를 직접 작성하므로 이름 없는 Named 쿼리 라 할 수 있음
    (JPA Named 쿼리처럼 애플리케이션 실행 시점에 문법 오류를 발견할 수 있음(매우 큰 장점!))

@Query, 값, DTO 조회하기

  • 단순 값을 하나만 조회 한다면
@Query("select m.username from Member m")
List<String> findUsernameList();
  • DTO로 직접 조회한다면
    ( DTO로 직접 조회 하려면 JPA의 new 명령어를 사용하는 것이 JPA와 사용방식이 동일하다.)
@Query("select new study.datajpa.dto.MemberDto(m.id, m.username, t.name) " + "from Member m join m.team t")
List<MemberDto> findMemberDto();

파라미터 바인딩

  • 가독성과 유지보수를 위해 이름 기반만 기억하자
select m from Member m where m.username = ?0 //위치 기반
select m from Member m where m.username = :name //이름 기반
  • 컬렉션 파라미터 바인딩은 in절을 통해 지원한다.
@Query("select m from Member m where m.username in :names")
List<Member> findByNames(@Param("names") List<String> names);

반환 타입

  • 스프링 데이터 JPA는 유연한 반환 타입 지원한다
List<Member> findByUsername(String name); //컬렉션
Member findByUsername(String name); //단건
Optional<Member> findByUsername(String name); //단건 Optional

조회 결과가 많거나 없으면?

  • 컬렉션
    결과 없음: 빈 컬렉션 반환

단건 조회

  • 결과 없음: null 반환(엔티티 단건) , Optional.Empty(Optional<>로 감쌌을 때)
  • 결과가 2건 이상: javax.persistence.NonUniqueResultException 예외 발생
  • IncorrectResultSizeDataAccessException은 각 DB마다 다른 종류의 데이터가 안맞는 예외를 보내도 추상화된 스프링 데이터에서 이것을 변환해서 예외를 반환한다.

그렇다면 조회 결과가 없는데 왜 null과 다른 값들을 반환할까?

  • 단건으로 지정한 메서드를 호출하면 스프링 데이터 JPA는 내부에서 JPQL의 Query.getSingleResult() 메서드를 호출한다.
  • Query.getSingleResult() 메서드를 호출하고 조회 결과가 없으면 javax.persistence.NoResultException 예외가 발생한다.
  • 스프링 데이터 JPA는 단건을 조회할 때 이 예외가 발생하면 예외를 무시하고 대신에 null 을 반환하도록 내부처리가 되어있음.

페이징과 정렬

1) 순수 JPA 페이징과 정렬

  • JPA 페이징 리포지토리 코드
public List<Member> findByPage(int age, int offset, int limit) {
    return em.createQuery("select m from Member m where m.age = :age order by
            m.username desc")
                    .setParameter("age", age)
                    .setFirstResult(offset)
                    .setMaxResults(limit)
                    .getResultList();
}

public long totalCount(int age) {
    return em.createQuery("select count(m) from Member m where m.age = :age",
                    Long.class)
            .setParameter("age", age)
            .getSingleResult();
}

2) 스프링 데이터 JPA 페이징과 정렬

  • 페이징과 정렬 파라미터
org.springframework.data.domain.Sort : 정렬 기능
org.springframework.data.domain.Pageable : 페이징 기능 (내부에 Sort 포함)
  • 특별한 반환 타입
org.springframework.data.domain.Page : 추가 *count 쿼리 결과를 포함*하는 페이징
org.springframework.data.domain.Slice : 추가 *count 쿼리 없이* 다음 페이지만 확인 가능 (내부적으로 limit + 1조회)
(무한 스크롤 생각하면 될 듯)
List (자바 컬렉션): 추가 count 쿼리 없이 결과만 반환
  • 페이징과 정렬 사용 예제
Page<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용
Slice<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용 안함
List<Member> findByUsername(String name, Pageable pageable); //count 쿼리 사용 안함
List<Member> findByUsername(String name, Sort sort)
  • spring data jpa 페이징 예제코드
public interface MemberRepository extends Repository<Member, Long> {
      Page<Member> findByAge(int age, Pageable pageable);
}

3) 위 두개의 테스트 코드 예제


// 순수 JPA 페이징 테스트 
    @Test
    public void paging() throws Exception {
        //given
        memberJpaRepository.save(new Member("member1", 10));
        memberJpaRepository.save(new Member("member2", 10));
        memberJpaRepository.save(new Member("member3", 10));
        memberJpaRepository.save(new Member("member4", 10));
        memberJpaRepository.save(new Member("member5", 10));
        int age = 10;
        int offset = 0;
        int limit = 3;
        
        //when
        List<Member> members = memberJpaRepository.findByPage(age, offset, limit);
        long totalCount = memberJpaRepository.totalCount(age);

        
        //페이지 계산 공식 적용...
        // totalPage = totalCount / size ...
        // 마지막 페이지 ...
        // 최초 페이지 ..
        //then
        
        assertThat(members.size()).isEqualTo(3);
        assertThat(totalCount).isEqualTo(5);
    }


    // spring data jpa 페이징 테스트
    //페이징 조건과 정렬 조건 설정
    @Test
    public void page() throws Exception {
        //given
        memberRepository.save(new Member("member1", 10));
        memberRepository.save(new Member("member2", 10));
        memberRepository.save(new Member("member3", 10));
        memberRepository.save(new Member("member4", 10));
        memberRepository.save(new Member("member5", 10));

        //when
        PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));
        Page<Member> page = memberRepository.findByAge(10, pageRequest);

        //then
        List<Member> content = page.getContent(); //조회된 데이터
        assertThat(content.size()).isEqualTo(3); //조회된 데이터 수
        assertThat(page.getTotalElements()).isEqualTo(5); //전체 데이터 수
        assertThat(page.getNumber()).isEqualTo(0); //페이지 번호
        assertThat(page.getTotalPages()).isEqualTo(2); //전체 페이지 번호
        assertThat(page.isFirst()).isTrue(); //첫번째 항목인가?
        assertThat(page.hasNext()).isTrue(); //다음 페이지가 있는가?
    }
  • 두 번째 파라미터로 받은 Pageable 은 인터페이스다. 따라서 실제 사용할 때는 해당 인터페이스를 구현한org.springframework.data.domain.PageRequest 객체를 사용한다
  • PageRequest 생성자의 첫 번째 파라미터에는 현재 페이지를, 두 번째 파라미터에는 조회할 데이터 수를
    입력한다. 여기에 추가로 정렬 정보도 파라미터로 사용할 수 있다. 참고로 페이지는 0부터 시작한다.

    주의: Page는 1부터 시작이 아니라 0부터 시작이다.

4) count 쿼리를 다음과 같이 분리할 수 있음

  • left join의 경우 count의 숫자는 그냥 select하는 것과 변함이 없는데 성능의 차이가 보인다.
    이를 해결하기 위해서 count 쿼리의 아래와 같이 분리하면 count할 때 간단한 쿼리가 따로 나간다.
    (실무에서는 무수히 많은 데이터가 무수히 많은 조인쿼리에 의해 성능을 크게 저하 시킬 수 있음. 무조건 최적화)
@Query(value = “select m from Member m”,
            countQuery = “select count(m.username) from Member m”)
Page<Member> findMemberAllCountBy(Pageable pageable);

5) 페이지를 유지하면서 엔티티를 DTO로 변환하기

  • JPA활용에서 했음. 절대 영속적인 영역인 entity를 view 영역까지 끌어가지 마라!
Page<Member> page = memberRepository.findByAge(10, pageRequest);
Page<MemberDto> dtoPage = page.map(m -> new MemberDto());

벌크성 수정 쿼리

스프링 데이터 JPA를 사용한 벌크성 수정 쿼리

@Modifying(clearAutomatically = true)
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
  • 벌크성 수정, 삭제 쿼리는 @Modifying 어노테이션을 사용한다
    사용하지 않으면 다음 예외 발생 -> org.hibernate.hql.internal.QueryExecutionRequestException: Not supported for DML operations
  • 벌크성 쿼리를 실행 후 영속성 컨텍스트 초기화: @Modifying(clearAutomatically = true) / (이 옵션의 기본값은 false )
    (JPA에서 벌크성 연산으로 영속성 컨텍스트를 안거치고 DB에 쿼리를 넣을 때 clear해야하는 이유와 같음)

@EntityGraph

  • 연관된 엔티티들을 SQL 한번에 조회하는 방법
  • member team은 지연로딩 관계일 때, N+1의 방법을 해결하는 SpringDataJPA의 방법
  • JPQL에서의 페치조인을 편리하게 도와주는 기능이라고 생각하면된다.
    (사용하면 JPQL 없이 페치 조인을 사용할 수 있다. (JPQL + 엔티티 그래프도 가능))
  • 예제
//공통 메서드 오버라이드
@Override
@EntityGraph(attributePaths = {"team"})
List<Member> findAll();

//JPQL + 엔티티 그래프
@EntityGraph(attributePaths = {"team"})
@Query("select m from Member m")
List<Member> findMemberEntityGraph();

//메서드 이름으로 쿼리에서 특히 편리하다.
@EntityGraph(attributePaths = {"team"})
List<Member> findByUsername(String username)

EntityGraph 정리

  • 사실상 페치 조인(FETCH JOIN)의 간편 버전
  • LEFT OUTER JOIN 사용

NamedEntityGraph(사용 안함)

  • EntityGraph를 JPA에서도 사용가능하다.
  • 하지만 Entityd와 메소드 둘 다 에다 어노테이션을 붙여주고 사용하다보니 생산성이 떨어진다.
  • 고로 해당 메소드에다가 명시해주는 페치조인을 사용하니 이것은 패스!
    (이런 것이 있다는 것만 알아놓자)

JPA Hint & Lock

JPA Hint란

  • JPA 쿼리 힌트(SQL 힌트가 아니라 JPA 구현체에게 제공하는 힌트)
    (JPA에서 각각의 구현체[하이버네이트 등]가 가지고 있는 고유 기능? 등을 발휘 할 수 있도록 제공 )
  • 쿼리 힌트 Page 추가 예제
@QueryHints(value = { @QueryHint(name = "org.hibernate.readOnly", value = "true")}, forCounting = true)
Page<Member> findByUsername(String name, Pageable pageable);
  • org.springframework.data.jpa.repository.QueryHints 어노테이션을 사용한다.

    위 예제 사용 상황
    A: 아니! 읽기만 하는데! 영속성 컨텍스트까지 DB의 데이터를 올려서 메모리 낭비를 해야겠습니까?!
    DB에서 읽기만 할테요!
    hibernate : 저희가 그럴줄 알고 그런 기능을 구현해놨으니 JPA Hint를 사용해서 DB에서 바로 읽으세요

    실사용 여부:
    힌트 조회로 얻는 성능상의 이점보다 복잡한 쿼리를 짜게 되어 잃게 되는 손해가 훨씬 크다.
    따라서, 제한된 시간에 저 코드를 하나하나 추가 하기보다 복잡한 쿼리를 풀어내는게 성능 개선에 더 이득일 수 있다.
    두개를 비교해서 이득인 방법으로 잘 계산해서 사용하라.

Lock

  • org.springframework.data.jpa.repository.Lock 어노테이션을 사용
  • JPA가 제공하는 락은 JPA 책 16.1 트랜잭션과 락 절을 참고
  • 실시간 트래픽이 많은 락을 걸어서는 안된다.
@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findByUsername(String name);
profile
일단 흐자

0개의 댓글