페이징 및 정렬
다음 조건으로 페이징, 정렬을 사용해보자
- 검색 조건: 나이 10살
- 정렬 조건: 이름 내림차순
- 페이징 조건: 첫 번째 페이지, 페이지당 보여줄 데이터 3건
순수 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();
}
-테스트
@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);
}
스프링 데이터 JPA 페이징과 정렬
페이징, 정렬 파라미터
- org.springframework.data.domain.Pageable : 페이징 기능(내부 Sort포함)
- org.springframework.data.domain.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);
다음 조건으로 페이징, 정렬을 사용해보자
- 검색 조건: 나이 10살
- 정렬 조건: 이름 내림차순
- 페이징 조건: 첫 번째 페이지, 페이지당 보여줄 데이터 3건
public interface MemberRepository extends JpaRepository<Member, Long> {
Page<Member> findByAge(int age, Pageable pageable);
}
@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.of(현재페이지번호, 조회할 데이터 사이즈, 정렬정보)
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); //현재 페이지 번호 (1아닌 0부터 시작!)
assertThat(page.getTotalPages()).isEqualTo(2); //총 페이지 수
assertThat(page.isFirst()).isTrue(); //첫번째 항목인가?
assertThat(page.hasNext()).isTrue(); //다음 페이지가 있는가?
}
두 번째 파라미터로 받은 Pageable은 인터페이스임.
따라서 실제 사용할 때는 해당 인터페이스를 구현한 org.springframework.data.domain.PageRequest객체를 사용함.
주의⚠️: Page 1부터가 아닌 0부터 시작.
Page 인터페이스
public interface Page<T> extends Slice<T> {
int getTotalPages(); //전체 페이지 수
long getTotalElements(); //전체 데이터 수
<U> Page<U> map(Function<? super T, ? extends U> converter); //변환기
}
Slice 인터페이스
public interface Slice<T> extends Streamable<T> {
int getNumber(); //현재 페이지
int getSize(); //페이지 크기
int getNumberOfElements(); //현재 페이지에 나올 데이터 수
List<T> getContent(); //조회된 데이터
boolean hasContent(); //조회된 데이터 존재 여부
Sort getSort(); //정렬 정보
boolean isFirst(); //현재 페이지가 첫 페이지 인지 여부
boolean isLast(); //현재 페이지가 마지막 페이지 인지 여부
boolean hasNext(); //다음 페이지 여부
boolean hasPrevious(); //이전 페이지 여부
Pageable getPageable(); //페이지 요청 정보
Pageable nextPageable(); //다음 페이지 객체
Pageable previousPageable(); //이전 페이지 객체
<U> Slice<U> map(Function<? super T, ? extends U> converter); //변환기
}
토탈 count 쿼리는 모든 데이터를 대상으로 수행되기 때문에, 성능 저하가 발생할 수 있음.
특히 카운트할때 불필요한 join이 포함된 경우, distinct, group by 등이 포함되어 있을 경우 더 비효율적
➡︎ 카운트 쿼리를 별도로 지정해 성능 최적화
@Query(value = "select m from Member m left join m.team t where m.age = :age",
countQuery = "select count(m.id) from Member m where m.age = :age")
Page<Member> findByAge(@Param("age") int age, Pageable pageable);
- value: 실제 데이터를 조회하는 쿼리
- countQuery: total count를 위한 쿼리
count(m)이 아니라 count(m.id)을 사용해 불필요한 join이나 연산 없이 단순하게 count만 수행하도록 분리 작성!
+)PageRequest안쓰고 List<Member> findTop3By();써도됨
페이지를 유지하면서 엔티티를 DTO로 변환
Page<Entity> 엔티티를 직접 API로 반환하지 않고, 반드시 Page<DTO>로 변환해서 반환해야함
//엔티티 조회
Page<Member> page = memberRepository.findByAge(10, pageRequest);
//DTO로 변환
Page<MemberDto> dtoPage = page.map(member -> new MemberDto(member.getId(), ...));
성능 최적화 관련 기능
벌크성 수정 쿼리
한번엔 데이터 변경할때
순수 JPA 벌크성 수정쿼리
public int bulkAgePlus(int age) {
int resultCount = em.createQuery("update Member m set m.age = m.age + 1 where m.age >= :age")
.setParameter("age", age)
.executeUpdate();
return resultCount;
}
스프링 데이터 JPA 벌크성 수정쿼리
@Modifying //벌크성 수정, 삭제 쿼리에 사용
@Query("update Member m set m.age = m.age +1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);
테스트
@Test
public void bulkUpdate() throws Exception{
//given
memberRepository.save(new Member("member1", 10));
memberRepository.save(new Member("member2", 19));
memberRepository.save(new Member("member3", 20));
memberRepository.save(new Member("member4", 21));
memberRepository.save(new Member("member5", 40));
//when
int resultCount = memberRepository.bulkAgePlus(20);
//then
assertThat(resultCount).isEqualTo(3);
}
벌크성 수정, 삭제 쿼리 @Modifying 사용
사용하지 않으면 org.hibernate.hql.internal.QueryExecutionRequestException: Not supported for DML operations 예외 발생
벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접쿼리날리기 때문에
영속성 컨텍스트에 있는 엔티티들은 이 변화 사실을 모름
벌크성 쿼리 실행 후 영속성 컨텍스트 초기화: @Modifying(clearAutomatically = true) (이 옵션 기본값 false)
만약 다시 조회해야 하면 꼭 영속성 컨텍스트를 초기화 하자!
권장 방안
1. 영속성 컨텍스트에 엔티티가 없는 상태에서 벌크 연산 먼저 실행
2. 부득이하게 영속성 컨텍스트에 엔티티가 있으면 벌크 연산 직후 영속성 컨텍스트를 초기화
@EntityGraph
연관된 엔티티들을 SQL 한번에 조회하는 방법
member team은 지연로딩 관계에서 team 조회시 N+1문제 발생 → 페치 조인 을 썼는데
@Query("select m from Member m left join fetch m.team")
List<Member> findMemberFetchJoin();
매번 fetch join 쿼리를 작성하는 건 귀찮고 중복도 많아짐, 메서드이름만으로 하고싶음 ➡︎ 엔티티 그래프(EntityGraph)
//공통 메서드 오버라이드
@Override
@EntityGraph(attributePaths = {"team"}) // ≒ 패치조인
List<Member> findAll();
//JPQL + 엔티티 그래프
@EntityGraph(attributePaths = {"team"})
@Query("select m from Member m")
List<Member> findMemberByEntityGraph();
//메서드 이름으로 쿼리
@EntityGraph(attributePaths = {"team"})
List<Member> findByUsername(@Param("username") String username);
EntityGraph 정리
- 사실상 페치 조인(FETCH JOIN)의 간편 버전
- LEFT OUTER JOIN 사용
NamedEntityGraph 사용 방법
(잘안씀)
@NamedEntityGraph(name = "Member.all", attributeNodes =
@NamedAttributeNode("team"))
@Entity
public class Member {}
@EntityGraph("Member.all")
@Query("select m from Member m")
List<Member> findMemberEntityGraph();
JPA Hint & Lock
가볍게 보기
JPA Hint
쿼리 날릴때 SQL 힌트가 아닌 JPA구현체(hibernate)에 주는 힌트
쿼리 힌트를 사용해 영속성 컨텍스트 동작 최적화 (예: 읽기 전용으로 성능 향상).
트랜잭션 커밋 시, 변경감지를 위해 스냅샷을 메모리에 저장하는데 비용(메모리+ 비교연산)이 듬
만약 조회만 할거라면? 변경 감지를 위한 스냅샷 자체가 불필요 → 이때 성능 최적화를 위해 힌트 줄 수 있음
쿼리 힌트 사용
@QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true"))
Member findReadOnlyByUsername(String username);
테스트
@Test
public void queryHint() throws Exception {
//given
memberRepository.save(new Member("member1", 10));
em.flush();
em.clear();
//when
Member member = memberRepository.findReadOnlyByUsername("member1");
member.setUsername("member2");
em.flush(); //Update Query 실행X
}
쿼리 힌트 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어노테이션을 사용
forCounting : 반환 타입으로 Page인터페이스를 적용하면 추가로 호출하는 페이징을 위한 count 쿼리도 쿼리 힌트 적용(기본값 true)
중요하고 이점있는 트래픽많은 api에 몇개 넣는거지 다 넣는게 아니라 성능테스트해보고 결정
JPA Lock
동시에 여러 사용자가 같은 데이터를 읽거나 쓸 때 충돌을 방지하기 위해
데이터에 잠금을 거는 기능 비관적(PESSIMISTIC) 또는 낙관적(OPTIMISTIC) 락을 설정.
@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findLockByUsername(String username);
- org.springframework.data.jpa.repository.Lock 어노테이션을 사용
- 실시간 트래픽이 많은 곳에서는 Lock을 걸면 안됨
@Test
public void lock() {
// given
Member member1 = memberRepository.save(new Member("member1", 10));
memberRepository.save(member1);
em.flush();
em.clear();
// when
List<Member> result = memberRepository.findLockByUsername("member1");
}
쿼리보면 select쿼리 뒤에 for update가 자동 붙음
커밋 또는 롤백 전까지 다른 트랜잭션이 해당 row를 수정하거나 삭제하지 못하게 막는 기능
락은 성능 저하, 데드락 위험이 있어서 정말 필요한 경우에만 사용
'Spring Data JPA' 카테고리의 다른 글
| [Spring Data JPA] 나머지 기능들 (0) | 2025.05.13 |
|---|---|
| [Spring Data JPA] 스프링 데이터 JPA 분석 (0) | 2025.05.12 |
| [Spring Data JPA] 확장 기능 (0) | 2025.05.10 |
| [Spring Data JPA] 쿼리 메소드 기능 (0) | 2025.05.08 |
| [Spring Data JPA] 공통 인터페이스 기능 (0) | 2025.05.02 |