TIL

[TIL] 20240305 JPA 페이징 기능

yjyj0101 2024. 3. 5. 22:07

1. 페이징 paging 이란?

  • 페이징 처리는 큰 데이터셋을 작은 '페이지'로 나누고, 각 페이지에는 고정된 수의 데이터 항목이 포함되어 있음
  • 전체 데이터셋에서 일부 데이터만을 조회하여 애플리케이션의 성능을 향상시킬 수 있으며, 사용자 인터페이스에서 데이터를 페이지 단위로 표시가능
  • 사용자 또는 애플리케이션은 한 번에 한 페이지의 데이터만 요청하며, 필요에 따라 다음 페이지나 이전 페이지의 데이터를 요청가능

 

2. 구현방법

(1) Repository 인터페이스 정의 및 PageRequest 를 사용하여 Pageable 객체화

PageRequest.of(int page int size, Sort sort, Direction direction, String ... props) :

 

(2) Pageable을 JpaRepository가 상속된 인터페이스의 메서드에 T(Entity)와 함꼐 파라미터로 전달 후, Page 반환

Page<User> usersByUserRole = userRepository
                .findUsersByUserRole(UserRole.SELLER, pageDTO.toPageable());

 

3. Page 객체의 주요 메서드

  • getContent(): 현재 페이지에 해당하는 데이터 리스트를 반환합니다.
  • getTotalElements(): 전체 데이터 수를 반환합니다.
  • getTotalPages(): 전체 페이지 수를 반환합니다.
  • getNumber(): 현재 페이지 번호를 반환합니다.
  • getNumberOfElements(): 현재 페이지에 있는 데이터 수를 반환합니다.
  • hasNext(), hasPrevious(), isFirst(), isLast(): 페이징 네비게이션 관련 정보를 제공합니다.

 

4. Pageable 응답형식

{
    "content": [
        {"id": 1, "username": "User 0", "address": "Korea", "age": 0},
        ...
        {"id": 5, "username": "User 4", "address": "Korea", "age": 4}
    ],
    "pageable": {
        "sort": {
            "sorted": false, // 정렬 상태
            "unsorted": true,
            "empty": true
        },
        "pageSize": 5, // 페이지 크기
        "pageNumber": 0, // 페이지 번호 (0번 부터 시작)
        "offset": 0, // 해당 페이지의 첫번째 요소의 전체 순번 (다음 페이지에서는 5)
        "paged": true,
        "unpaged": false
    },
    "totalPages": 20, // 페이지로 제공되는 총 페이지 수
    "totalElements": 100, // 모든 페이지에 존재하는 총 원소 수
    "last": false,  // 마지막 페이지 여부
    "number": 0,
    "sort": {
        "sorted": false,    // 정렬 사용 여부
        "unsorted": true,
        "empty": true
    },
    "size": 5,       // Contents 사이즈
    "numberOfElements": 5,  // Contents 의 원소 수
    "first": true,   // 첫페이지 여부
    "empty": false   // 공백 여부
}

 

5. 페이지 반환 타입 (3가지)

Page<T> 게시판 형태의 페이징, totalelement도 같이 조회함
Slice<T> 더보기 형태의 페이징, 전체 요소 갯수 대신 offset 필드로 조회 (단, 성능이 안좋아서 limit+1로 사용)
List<T> 전체 목록보기 형태의 페이징, count 조회가 발생하지 않음

 

+) 전체 count 쿼리가 추가로 발생하는 Page<T> 보다는 List<T>가 대용량 처리할때 더 안정적

 

6. 페이징 정렬

Sort 클래스 / JpaSort / Query 이용

//pageable
Pageable pageable = PageRequest.of(pageNumber, pageSize, Sort.by("someField").ascending());
Page<MyEntity> resultPage = myRepository.findAll(pageable);
//Sort 클래스
Sort sort = Sort.by(Sort.Order.asc("field1"), Sort.Order.desc("field2"));
Pageable pageable = PageRequest.of(pageNumber, pageSize, sort);
Page<MyEntity> resultPage = myRepository.findAll(pageable);
//JpaSort
public interface UserRepository extends JpaRepository<User, Long> {

    @Query("SELECT u FROM User u WHERE u.department.id = :departmentId")
    List<User> findByDepartmentIdSortedByUserNameLength(
        @Param("departmentId") Long departmentId, 
        Sort sort);
}

Sort sort = JpaSort.unsafe("LENGTH(u.name)");
List<User> users = userRepository.findByDepartmentIdSortedByUserNameLength(departmentId, sort);

 

 

 

 

7. PageDTO 클래스

- `currentPage`: 현재 페이지 번호
- `pageSize`: 한 페이지에 표시할 게시글의 수
- `totalElements`: 전체 게시글 수
- `totalPages`: 전체 페이지 수, `totalElements`를 `pageSize`로 나눈 후 올림하여 계산
- `dataList`: 현재 페이지에 표시할 데이터의 리스트
- `startPage`, `endPage`: 페이징 바에서 표시할 시작 페이지 번호와 끝 페이지 번호
- `hasPreviousPage`, `hasNextPage`: 이전 페이지 또는 다음 페이지가 존재하는지의 여부

### 계산 방법

1. **전체 페이지 수 계산**:

   totalPages = (int) Math.ceil((double) totalElements / pageSize);


2. **시작 페이지와 끝 페이지 계산**:
   - 페이징 바에서 몇 개의 페이지를 표시할지 결정한다.
   	 예를 들어, 한 번에 최대 5개의 페이지 번호를 표시한다고 가정하면,
   - 현재 페이지 그룹의 시작 페이지를 계산한다. 현재 페이지가 1~5이면 시작 페이지는 1, 6~10이면 시작 페이지는 6이 된다.
     
     startPage = ((currentPage - 1) / 5) * 5 + 1;
     endPage = Math.min(startPage + 4, totalPages);
     ```
   
3. **이전 페이지 / 다음 페이지 존재 여부 계산**:

   hasPreviousPage = currentPage > 1;
   hasNextPage = currentPage < totalPages;
   

4. **현재 페이지의 데이터 리스트 추출**:
   - 데이터베이스 쿼리나 데이터 소스에서, 현재 페이지에 해당하는 데이터 리스트를 `pageSize`와 `currentPage`를 기반으로 계산하여 추출
   - 예를 들어, JPA에서는 `PageRequest`를 사용하여 간단히 처리가능

     PageRequest pageRequest = PageRequest.of(currentPage - 1, pageSize);
     Page<T> page = repository.findAll(pageRequest);
     dataList = page.getContent();

 

 

8. 실습코드

public class PageDTO<T> {
    private int currentPage;
    private int pageSize;
    private long totalElements;
    private int totalPages;
    private List<T> dataList;
    private int startPage;
    private int endPage;
    private boolean hasPreviousPage;
    private boolean hasNextPage;
}
@Service
public class PostService {

    @Autowired
    private PostRepository postRepository;

    public PageDTO<Post> getPostsPage(int currentPage, int pageSize) {
        long totalElements = postRepository.count(); // 전체 게시글 수
        int totalPages = (int) Math.ceil((double) totalElements / pageSize); // 전체 페이지 수
        
        // 데이터 리스트 조회
        PageRequest pageRequest = PageRequest.of(currentPage - 1, pageSize);
        Page<Post> page = postRepository.findAll(pageRequest);
        List<Post> dataList = page.getContent();
        
        // PageDTO 초기화
        PageDTO<Post> pageDTO = new PageDTO<>();
        pageDTO.setCurrentPage(currentPage);
        pageDTO.setPageSize(pageSize);
        pageDTO.setTotalElements(totalElements);
        pageDTO.setTotalPages(totalPages);
        pageDTO.setDataList(dataList);
        // startPage, endPage, hasPreviousPage, hasNextPage 계산 로직은 구현에 따라 달라질 수 있음
        
        return pageDTO;
    }
}
@RestController
@RequestMapping("/posts")
public class PostController {

    @Autowired
    private PostService postService;

    @GetMapping
    public ResponseEntity<PageDTO<Post>> getPosts(
            @RequestParam(defaultValue = "0") int page,
            @RequestParam(defaultValue = "10") int size) {
        PageDTO<Post> pageDTO = postService.getPostsPage(page, size);
        return ResponseEntity.ok(pageDTO);
    }
}