후라이

Spring Pageable 안 쓰면 바보 본문

Spring

Spring Pageable 안 쓰면 바보

힐안 2025. 1. 31. 17:56

1. 개요

만약 Post(게시글)이 1,000개 존재한다고 하면, 이 1,000개의 데이터를 한 번에 가져올 시  부하가 심하게 걸리게 됩니다.

그렇다면, 데이터를 한 번에 모두 가져오지 않고, 특정 크키만큼 데이터를 나눠서 조회하는 방식을 사용해야겠죠?

이것이 페이징(Pagination)입니다.

 

2. Pageable

페이징(Pagination) 기법을 스프링에서 사용자가 직접 구현해서 사용할 수도 있지만, Spring에서 제공하는 Pageable을 사용할 수도 있겠습니다. PageableSpring Data JPA에서 페이징과 정렬을 쉽게 처리할 수 있도록 제공하는 인터페이스입니다. 

 

 

PageRequest가 실제 구현체인데, PageRequest의 생성자는 pageNumber, pageSize, sort를 사용합니다.

 

pageNumber : size를 기준으로 몇 번째 페이지의 데이터를 가져올지
pageSize : 한 페이지에 가져올 데이터의 개수
sort : 특정 컬럼을 기준으로 오름차순/내림차순 정렬

 

추가로, 자동으로 COUNT 쿼리를 실행하는데, Page<T> 객체를 사용할 경우 전체 데이터 개수를 자동으로 계산합니다.

 

3. Pageable의 핵심 메서드

 

메서드 설명
getPageNumber() 현재 페이지 번호 반환(0부터 시작)
getPageSize() 한 페이지에 포함된 데이터 개수 반환
getSort() 정렬 정보 반환
isPaged() 페이징이 적용되었는지 확인
next() 다음 페이지 정보 반환
previousOrFirst() 이전 페이지 정보 반환
first() 첫 번째 페이지 정보 반환 

 

 

4. Pageable 적용

 

Controller

    @GetMapping("/welcome")
    public String welcomePage(Model model, @PageableDefault(sort = "id", direction = Sort.Direction.DESC)
                              Pageable pageable, @LoginUser UserDto.Response user) {
        Page<Post> list = postService.pageList(pageable);

        if(user!=null) {
            model.addAttribute("user",user);
        }
        model.addAttribute("posts", list);
        model.addAttribute("previous", pageable.previousOrFirst().getPageNumber());
        model.addAttribute("next", pageable.next().getPageNumber());
        model.addAttribute("hasNext", list.hasNext());
        model.addAttribute("hasPrev", list.hasPrevious());

        return "main";
    }

 

컨트롤러 인자에 Pageble이 들어간 것을 확인할 수 있습니다.

@PageableDefault(sort="id", direction = Sort.Direction.DESC) : 기본적으로 id 컬럼을 기준으로 내림차순(DESC) 정렬

즉, 최신 게시글이 제일 먼저 보이도록 설정합니다.

 

@PageableDefault란?
Spring Data JPA에서 페이징 요청 시 기본값을 설정할 때 사용하는 어노테이션이다.
사용자가 요청에서 number, size, sort 파라미터를 명시하지 않았을 경우, 기본값을 적용하게 된다.
size : 한 페이지당 불러올 데이터 개수 (기본값 20)
page : 기본 페이지 번호 (0부터 시작) (기본값 0)
sort : 정렬 기준 컬럼 (지정하지 않으면 기본 정렬 없음)
direction : 정렬 방향 (ASC, DESC)

 

그리고, 앞서 말한 메서드를 통해 이전/다음 페이지 정보를 추가합니다.

  • previous : 이전 페이지 번호
  • next : 다음 페이지 번호
  • hasNext : 다음 페이지 존재 여부
  • hasPrev : 이전 페이지 존재 여부

 

Service

    @Transactional(readOnly = true)
    public Page<Post> pageList(Pageable pageable) {
        return postRepository.findAll(pageable);
    }

 

서비스에서 비즈니스 로직을 수행하면서 Spring Data Repository에 Pageable을 또 인자로 함께 넘겨줍니다.

게시글 전체를 페이징 처리하여 가져오기 위해 Page<Post>를 반환합니다.

 

 

Repository

@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
    Page<Post> findByTitleContaining(String keyword, Pageable pageable);
}

 

findAll(Pageable pageable) 에서 전체 게시글을 페이징하여 조회하도록 하며,

findByTitleContaining(String keyword, Pageable pageble) 로 특정 키워드가 포함된 제목의 게시글만 페이징하여 조회하는 추가 기능을 통해 검색 로직을 구현할 수도 있습니다.

 

5. 페이징 요청과 응답 예시

 

GET /welcome?page=1&size=5

 

page : 1번 페이지 (두 번째 페이지) 요청

size : 한 페이지당 5개의 게시글 가져오기

 

SELECT * FROM post 
ORDER BY id DESC 
LIMIT 5 OFFSET 5;

 

위와 같은 SQL이 실행된다고 생각하면 쉽습니다.

 

{
    "content": [
        { "id": 10, "title": "새로운 공지사항" },
        { "id": 9, "title": "이벤트 안내" },
        { "id": 8, "title": "서비스 점검 일정" },
        { "id": 7, "title": "업데이트 내용" },
        { "id": 6, "title": "회원가입 혜택" }
    ],
    "totalElements": 12,
    "totalPages": 3,
    "number": 1,
    "size": 5,
    "hasNext": true,
    "hasPrevious": true
}

 

우리는 위와 같은 응답 JSON 데이터로 Page<Post>을 받을 수 있습니다.

 

content : 현재 페이지의 게시글 목록

totalElements : 총 12개의 게시글

totalPages : 전체 페이지수 3개

number : 현재 페이지 번호

hasNext : 다음 페이지가 존재

hasPrevious : 이전 페이지가 존재

 

📌 참고
Pageable과 메서드 네임 기반 정렬(OrderBy)을 함께 사용하게 되면 Spring Data JPA에서는 메서드 네임의OrderBy가 우선 적용되게 됩니다. createdAt 정렬이 강제 적용되기 때문에 Pageable의 정렬 조건(Sort.by("id).descending())은 무시됩니다.

1️⃣ 메서드 네임에서 OrderBy를 사용하면, Pageable의 정렬이 무시됨.
2️⃣ 메서드 네임의 OrderBy를 제거하면 Pageable의 정렬이 정상적으로 동작.
3️⃣ 메서드에서 정렬을 강제하고 싶다면 OrderBy를 유지, 동적으로 변경하고 싶다면 Pageable을 활용.