[TIL] 페이징 (feat.Spring Data Jpa)

이형석·2024년 9월 21일

페이징 처리하는 법을 최대한 간단하게 정리해보았다

먼저 상황을 서술하자면, 게시글이 DB에 쌓여있다. 그리고 사용자가 요청한 페이지에 필요한 만큼의 게시글들을 찾아서 응답해주려 한다.


Pageable, Page 개념

참고 링크 https://sm-studymemo.tistory.com/141

Pageable 인터페이스

클라이언트의 페이징 요청을 처리하는데 필요한 정보가 담김

  • int page, int size, Sort sort (시작페이지, 데이터갯수, 정렬기준)
  • Pageable 구현체 : PageRequest

Page 인터페이스

요청에 의해 처리된 페이지 정보가 담김

  • 요청 데이터(게시글들), 총 데이터 갯수, 총 페이지 갯수, 현재 페이지, ...
  • Page 구현체 : PageImpl

페이징 처리 과정

1. 클라이언트로부터 페이징 처리 정보(Pageable)를 받음

2. JpaRepository에게 Pageable로 처리된 페이지 정보(Page)를 요청함

3. 클라이언트에게 처리된 페이지 정보(Page)를 응답


사용예시

알고가기
Spring Data JPA 리포지토리에
Pagable을 매개변수로 받아, Page를 리턴해주는 메서드가 이미 존재함(기본 CRUD 메서드처럼)
-> findAll(Pageable pageable);

1. 리포지토리

public PostRepository extends JpaRepository<Post, Long>{
	// 만약 유저로 찾고싶으면, 아래 메서드 추가
	Page<Post> findByUser(User user, Pageable pageable);	// 메서드명, 파라미터 순서 지키기
}

2. 서비스

public Page<Post> findAll(Pageable pageable){
	return postRepository.findAll(pageable);
}

3. 컨트롤러

@GetMapping("/post")
public ResponseEntity<Page> find(Pageable pageable){
	Page<Post> postPage = postService.findAll(pageable);
    
	//Page의 getContent()에 엔터티들이 들어있음. 모두 DTO로 변환해주기.
    //Page는 불변객체이므로, 새 Page 객체를 생성하여 사용.
    List<PostDTO> postDtos = postPage.getContent().stream()
                                      .map(PostDTO::new) // Post를 PostDTO로 변환
                                      .collect(Collectors.toList());
    Page<PostDTO> postDtoPage = new PageImpl<>(postDtos, pageable, postPage.getTotalElements());
    
    return ResponseEntity.ok(postDtoPage);
}

클라이언트가 Page를 처리하도록, Page를 스프링 웹 MVC가 JSON 및 메타데이터로 변환하여 응답해줌 (따라서 위 코드처럼 ResponseEntity에 Page 그대로 담아서 응답해도 됨)

4. URL 요청

ex) http://localhost:8080/post?page=0&size=5&sort=title,asc
  • 쿼리스트링이 @ModelAttribute에 의해, Pageable객체로 매핑됨
  • title은 엔터티의 필드명
  • sort 생략가능

페이징시 N+1 문제 처리 방법

@ManyToOne 관계에서 처리

방법 1. 직접 리포지토리에 페이징처리용 페치조인 쿼리 작성

public interface PostRepository extends JpaRepository<Post, Long> {
    @Query(value = "select p from Post p join fetch p.user", 
           countQuery = "select count(p) from Post p")
    Page<Post> findAllWithUser(Pageable pageable);
}

방법 2. @EntityGraph 사용

public interface PostRepository extends JpaRepository<Post, Long> {
	@EntityGraph(attributePaths = {"user"})	//Post의 필드명
	@Query("select p from Post p")
	Page<Post> findAllWithUser(Pageable pageable);
}

@OneToMany 관계에서 처리

OneToMany 관계에서 페치조인 사용시 문제가 발생함
페이지네이션은 SQL 결과 테이블의 row 단위로 처리함. 그런데 일대다 조인에서는 같은 엔터티가 중복된 row들이 나옴. 따라서 문제가 발생함.

결과 테이블 ex)
		유저	게시글
1		user1	post1
2		user1	post2
3		user1	post3

그럼 JPQL의 distinct를 쓰면 안되나? 이유는 모르겠는데 안된다고 함(왜 안되는 지 나중에 질문해보기)

방법 1. 옵션을 통해, 지연로딩 (LAZY) 동작을 최적화

  1. 컬렉션은 지연 로딩으로 조회하기
  2. 지연로딩 성능 최적화를 위한 application.properties에 옵션을 설정
jpa.hibernate.default_batch_fetch_size=1000

# 컬렉션의 갯수가 n개라면, 원래는 1 + n 번 지연조회가 일어남. 그런데 이 n을 한 번에 1000개씩 묶어서 조회해줌
# 만약 컬렉션의 갯수가 2000개면 1 + 2 번 조회가 일어남
# SQL의 IN절을 사용함
# size의 최댓값은 1000임, 단 부하가능성이 있으므로 100~1000개 사이로 지정, 애매하면 500으로 사용

방법 2. 어노테이션을 통해, 지연로딩 (LAZY) 동작을 최적화

@BatchSize로 엔터티의 컬렉션 필드 또는, 엔터티 클래스 단위에 개별 지정하기

//클래스 단위로 사용시
@BatchSize(size = 500)
class User{
	//필드 단위로 사용시
    @BatchSize(size = 500)
    @OneToMany(mappedBy = "user")
    private List<Post> posts = new ArrayList<>();
}
profile
금융IT 개발자

0개의 댓글