본문 바로가기

백엔드/JPA

스프링 데이터 jpa 기본 기능정리

파라미터 바인딩

파라미터 바인딩에는 위치 기반과 이름 기반이 있다. 이름기반을 사용하자!! (유지보수와 가독성이 좋음)

@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);

컬렉션을 파라미터로 넘길때도 아래와 같이 가능하다.

@Query("select m from Member m where m.username in :names")
List<Member> findByNames(@Param("names") List<String> names);

 

반환값 & 페이징과 정렬

List<Member> findMemlistByUsername(String name);
Member findOneByUsername(String name);
Optional<Member> findOptionalByUsername(String name);

스프링 데이터 레포지토리에 추가

@Test
public void returnType() {
    Member member1 = new Member("M11",10);
    Member member2 = new Member("M11",20);
    Member member3 = new Member("AAA",12);
    memberRepository.save(member1);
    memberRepository.save(member2);
    memberRepository.save(member3);

    List<Member> memList = memberRepository.findMemlistByUsername("M11");
    Optioanl<Member> opMem = memberRepository.findOptionalByUsername("AAA");
}

반환값이 없을 수 있기 때문에 Optional로 감싸주는 것이 좋다. Optional로 찾아오는 sql 쿼리의 결과가 2개 이상이면 에러가 발생할 것이다.

👉 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();
    }
@Test
public void test() {
    Member member1 = new Member("M11",10);
    Member member2 = new Member("M12",10);
    Member member3 = new Member("AAA",10);
    Member member4 = new Member("ABBAA",10);
    Member member5 = new Member("a",10);

    memberRepository.save(member1);
    memberRepository.save(member2);
    memberRepository.save(member3);
    memberRepository.save(member4);
    memberRepository.save(member5);

    List<Member> byPage = memberJpaRepo.findByPage(10, 0, 3);
    assertThat(byPage.size()).isEqualTo(3);
}

 

👉 스프링 데이터 jpa에서의 페이징

//레포지토리 코드 추가
Page<Member> findByAge(int age, Pageable pageable);
@Test
    public void pagingTest(){
        Member member1 = new Member("M11",10);
        Member member2 = new Member("M12",10);
        Member member3 = new Member("AAA",10);
        Member member4 = new Member("ABBAA",10);
        Member member5 = new Member("a",10);

        memberRepository.save(member1);
        memberRepository.save(member2);
        memberRepository.save(member3);
        memberRepository.save(member4);
        memberRepository.save(member5);

        PageRequest pageReq = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));

        //when
        Page<Member> page = memberRepository.findByAge(10, pageReq);

        //then
        List<Member> content = page.getContent();
        long totalElements = page.getTotalElements();
    }

테스트는 이렇게 구성할 수 있다. 한가지 재밌는 점은

페이지 계산을 위해 count 쿼리도 나간다는 것이다. 스프링 데이터 jpa 가 제공하는 페이징에는 부가적인 정보들이 많이 있다. 이를 확인해보기 위해 위의 테스트 코드 아랫부분에 다음의 코드를 추가한다.

List<Member> content = page.getContent();
long totalElements = page.getTotalElements();

assertThat(content.size()).isEqualTo(3);
assertThat(page.getTotalElements()).isEqualTo(5);
assertThat(page.getNumber()).isEqualTo(0); //페이지 번호
assertThat(page.getTotalPages()).isEqualTo(2); //총 페이지 번호
assertThat(page.isFirst()).isEqualTo(true);
assertThat(page.hasNext()).isEqualTo(true);

Page클래스를 들어가보면 Slice 클래스를 상속하고 있음을 알 수 있다. Slice 클래스에는 getTotalElements, getTotalPage 등 페이지에 관한 기능이 없다. 그리고 한가지 재밌는 점은 Page클래스를 사용하면 limit 쿼리가 지정한 값만큼 그대로 나간다!

하지만 코드를 다음과 같이 바꾸면

//레포지토리에서 반환값을 Page -> Slice로 변경
Slice<Member> findByAge(int age, Pageable pageable);


//테스트 코드도 그에 맞게 반환 타입을 Slice로 변경
Slice<Member> page = memberRepository.findByAge(10, pageReq);

        List<Member> content = page.getContent();
//        long totalElements = page.getTotalElements();

        assertThat(content.size()).isEqualTo(3);
//        assertThat(page.getTotalElements()).isEqualTo(5);
        assertThat(page.getNumber()).isEqualTo(0); //페이지 번호
//        assertThat(page.getTotalPages()).isEqualTo(2); //총 페이지 번호
        assertThat(page.isFirst()).isEqualTo(true);
        assertThat(page.hasNext()).isEqualTo(true);

다음 페이지가 있는지 확인하기 위해 한개 더 많이 조회해온다.

🧐 count 쿼리 성능 관련

total count는 데이터가 많고 조인이 복잡할수록 성능이 안나올 수 있다. 그래서 count쿼리는 따로 지정할 수 있다.

@Query(value="select m from Member m left join m.team t")
Page<Member> findByAge(int age, Pageable pageable);

만약 이렇게 되어있다면 select쿼리랑 Page 클래스가 하는 count 쿼리 두개 모두 위의 left join을 하는 쿼리가 나갈 것이다.

실무에서 만약 여러 테이블을 join하는 상황으로 이어진다면 성능이 많이 안좋아질 수 있다. 그래서 아래와 같이 count 쿼리는 따로 지정할 수 있다.

@Query(value="select m from Member m left join m.team t", countQuery = "select count(m.username) from Member m")
Page<Member> findByAge(int age, Pageable pageable);

count 쿼리가 많이 단순해졌다!!

 

😎 page는 추가로 map 함수를 제공하는데 엔티티를 DTO로 변환할때 사용하면 유용하다.

//테스트 코드
Page<Member> page = memberRepository.findByAge(10, pageReq);

Page<MemberDto> map = page.map(m -> new MemberDto(m.getId(), m.getUsername(), null));

 

정리

반환타입은 3가지

  • Page : 추가 count 쿼리 결과를 포함하는 페이징
  • Slice : 추가 count 쿼리 없음. 다음 페이지 유무만 확인가능 (limit + 1 조회)
  • List : 일반적인 컬렉션

페이징의 인덱스는 0부터이다

 

불크 연산

jpa에서의 불크연산

public int bulkUpdateAge(int age){
    return em.createQuery("update Member m set m.age = m.age+1 where m.age >= :age")
            .setParameter("age", age).executeUpdate();
}

이전에 배웠던 jpa에서의 불크연산은 이렇게 했었다.

 

스프링 데이터 jpa의 불크연산

@Modifying //이게 있어야 update 실행
@Query("update Member m set m.age = m.age+1 where m.age >= :age")
int bulkUpdate(@Param("int") int age);
  1. bulk 연산은 1차캐시에 반영되지 않으므로 영속성 컨텍스트를 초기화해야한다.
  2. em.flush는 하지 않아도 된다. (JPQL 직전에 flush)
  3. em.clear 만 해주면 되는데 스프링데이터 jpa는 @Modifying(clearAutomatically=true)
@Modifying(clearAutomatically=true)
@Query("update Member m set m.age = m.age+1 where m.age >= :age")
int bulkUpdate(@Param("age") int age);

결론은 위와같이 쓰면 된다.

 

페치조인

스프링 데이터 jpa에서는 페치조인을 쓰려면 레포지토리에 @Query로 넣어주면 된다.

@Query("select m from Member m left join fetch m.team")
List<Member> findMemberFetch();

복잡한 쿼리는 이렇게 하면 되지만 간단한 쿼리까지 매번 이렇게 하는 것은 번거로우므로 스프링 데이터 jpa에서는 더 나은 방법을 제공한다.

👉 메서드 이름으로 해결 + 페치조인

@Override
@EntityGraph(attributePaths = {"team"})
List<Member> findAll();

@EntityGraph(attributePaths = {"team"})
@Query("select m from Member m")
List<Member> findAll2();

@EntityGraph(attributePaths = {"team"})
List<Member> findByUsername(@Param("username") String username);

엔티티 그래프 어노테이션을 사용하면 메서드 이름으로 쿼리를 만들어내면서 페치조인을 할 수 있다. 이와 같은 방법은 복잡한 쿼리에는 적합하지 않고 그냥 간단한 쿼리의 경우에 사용하는 것이다.

JPA Hint 와 Lock

👉 Hint

jpa로 select 쿼리를 날려서 가져오면 영속성 컨텍스트에는 항상 더티 체킹을 위한 객체를 따로 만들어둔다. 복사본을 만드는 건데 만약 읽기전용으로만 가져올 것이라면 이는 비효율적일 수 있다. 그런 경우에 사용한다!

@QueryHints(value = @QueryHint(name="org.hibernate.readOnly", value ="true"))
Member findReadOnlyByUsername(String name);

이렇게 하면 읽기전용으로 가져와서 변경이 일어나도 update문이 나가지 않는다.

정리 : 이 방법은 사실 크게 성능이 좋아지진 않아서 매번 이렇게 할 필요는 없다. 그냥 엄청 트래픽 큰 api정도만 하자.

 

👉 Lock

@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findLockByUsername(String n);

실시간 트래픽이 많은 서비스는 락을 걸면 안된다.
실시간 트래픽보다는 돈을 맞춰야하는 건 위의 락을 걸면 좋은 방법일 수 있다.

 

관련글

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%8D%B0%EC%9D%B4%ED%84%B0-JPA-%EC%8B%A4%EC%A0%84/dashboard

 

실전! 스프링 데이터 JPA - 인프런 | 강의

스프링 데이터 JPA는 기존의 한계를 넘어 마치 마법처럼, 리포지토리에 구현 클래스 없이 인터페이스 만으로 개발을 완료할 수 있습니다. 그리고 반복 개발해온 기본 CRUD 기능도 모두 제공합니다

www.inflearn.com