[Spring Boot] JPA + Pageable 을 이용한 페이징 처리

안녕하세요. 남산돈가스 입니다.

오늘은 Spring Boot JPA를 이용하여 API 개발 시 간단하게 Pagination 와 Sorting을 처리할 수 있도록 도와주는 Pageable에 대해서 알아보려고 합니다.

웹 개발 시 Pagination 과 Sorting은 필수적이라 할 수 있지만, 실제 개별적으로 구현 시 번거로운 작업이 생기기 마련입니다. 또한 각 데이터베이스마다 페이징 쿼리가 다를 수 있다는 점에서 모든 요구조건을 만족하기 어려운 부분들이 존재하기 마련입니다.

Spring Data JPA와 Pageable을 이용하면 이런 문제들을 아주 쉽게 해결할 수 있어 비즈니스 로직에 집중할 수 있게 도와줍니다.

우선 Pageable을 사용하여 얻을 수 있는 이점은 대표적으로 두 가지 입니다.
  1. 요건에 맞는 Pagination을 구현할 수 있다.
  2. 정렬이 필요한 데이터를 쉽게 Sorting 할 수 있다.
예제를 들어 설명하겠습니다.

@Entity
@Table(name="post")
@Getter
public class Post extends Audity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name="id")
    private Long id;

    @Column(name="title")
    private String title;

    @Column(length = 2000, name="content")
    private String content;

    @Column(length = 50, name="writer")
    private String writer;
}

간단한 예제를 들기 위하여, "Post" 라는 Entity를 작성하였고, 예제를 위하여 약 20건의 데이터를 생성해두었습니다.

@Repository
public interface PostRepository extends JpaRepository<Post, Long> {
}

생성한 Post Entity를 접근하기 위하여 JpaRepository를 상속받는 PostRepository를 생성합니다.


위 다이어그램에서와 같이 생성한 PostRepository는 JpaRepository를 상속받았고, 또 JpaRepository는 PagingAndSortingRepository를 상속받은 것을 확인할 수 있습니다.
이렇게 PagingAndSortingRepository만 상속받은 Repository를 생성한다면, Pageable을 매개변수로 Pagination / Sorting 을 처리할 준비가 끝난 것 입니다.

@RestController
@RequestMapping("/posts")
public class PostController {

    @Autowired
    private PostRepository postRepository;

    @GetMapping
    public ResponseEntity retrievePosts(final Pageable pageable) {
        Page<Post> posts = postRepository.findAll(pageable);
        return new ResponseEntity<>(posts,HttpStatus.OK);
    }
}

그럼 이제, 외부에서 Post를 조회할 수 있도록 Controller를 생성합니다.
이 예제에서는 블로그 목적에 맞게 Pageable을 설명하기 위함이므로 Controller -> Repository 를 호출하는 구조로 작성하였습니다. 실제 구현 시에는 Service와 목적에 맞는 DTO를 생성하여 처리하는 것이 안전합니다.
- 파라미터 정보 없이 요청한 경우




{
    "content": [
        {
            "createdDate": "2020-03-13T07:05:46",
            "updatedDate": "2020-03-13T07:05:46",
            "id": 1,
            "title": "1번 게시글",
            "content": "안녕하세요",
            "writer": "남산돈가스"
        },
        {
            "createdDate": "2020-03-13T08:05:46",
            "updatedDate": "2020-03-13T07:05:46",
            "id": 2,
            "title": "2번 게시글",
            "content": "안녕하세요",
            "writer": "남산돈가스"
        },
        {
            "createdDate": "2020-03-13T09:05:46",
            "updatedDate": "2020-03-13T07:05:46",
            "id": 3,
            "title": "3번 게시글",
            "content": "안녕하세요",
            "writer": "명동교자\n"
        },
        {
            "createdDate": "2020-03-13T10:05:46",
            "updatedDate": "2020-03-13T07:05:46",
            "id": 4,
            "title": "4번 게시글",
            "content": "안녕하세요",
            "writer": "남산돈가스"
        },
        {
            "createdDate": "2020-03-13T11:05:46",
            "updatedDate": "2020-03-13T07:05:46",
            "id": 5,
            "title": "5번 게시글",
            "content": "안녕하세요",
            "writer": "남산타워"
        },
        {
            "createdDate": "2020-03-13T12:05:46",
            "updatedDate": "2020-03-13T07:05:46",
            "id": 6,
            "title": "6번 게시글",
            "content": "안녕하세요",
            "writer": "남산돈가스"
        },
        {
            "createdDate": "2020-03-13T13:05:46",
            "updatedDate": "2020-03-13T07:05:46",
            "id": 7,
            "title": "7번 게시글",
            "content": "안녕하세요",
            "writer": "남산돈가스"
        },
        {
            "createdDate": "2020-03-13T14:05:46",
            "updatedDate": "2020-03-13T07:05:46",
            "id": 8,
            "title": "8번 게시글",
            "content": "안녕하세요",
            "writer": "명동교자\n"
        },
        {
            "createdDate": "2020-03-13T15:05:46",
            "updatedDate": "2020-03-13T07:05:46",
            "id": 9,
            "title": "9번 게시글",
            "content": "안녕하세요",
            "writer": "남산돈가스"
        },
        {
            "createdDate": "2020-03-13T16:05:46",
            "updatedDate": "2020-03-13T07:05:46",
            "id": 10,
            "title": "10번 게시글",
            "content": "안녕하세요",
            "writer": "남산돈가스"
        }
    ],
    "pageable": {
        "sort": {
            "sorted": false,
            "unsorted": true,
            "empty": true
        },
        "offset": 0,
        "pageSize": 10,
        "pageNumber": 0,
        "paged": true,
        "unpaged": false
    },
    "totalElements": 18,
    "last": false,   // 마지막 페이지 여부 
    "totalPages": 2,   // 전체 페이지 갯수 총 18건 데이터 중 10개의 요청이므로 2개 페이지 존재
    "size": 10,    // 페이지 당 출력 갯수  
    "number": 0,  
    "numberOfElements": 10,  // 요청 페이지에서 조회 된 데이터의 갯수
    "sort": {
        "sorted": false,
        "unsorted": true,
        "empty": true
    },
    "first": true,   // 첫 페이지 여부
    "empty": false
}

위 와 같이 contents 에는 실제 DB로부터 요청한 결과가 보여지며, 그 이외는 Paging 결과를 확인할 수 있는 값들이 다양하게 표현되어 있습니다. 이 값들을 가지고 공통적으로 처리되는 페이징 결과를 만들어 처리할 수 있을 것 같습니다.

그럼 이제 실제 요청 값에 페이징 정보를 전달해보겠습니다.
Controller의 매개변수인 Pageable은 Spring MVC가 요청파라미터로부터 기본 설정 된 파라미터 값이 존재하면 Pageable 객체를 생성하려고 시도합니다. 이때 기본 설정 된 파라미터는 아래와 같습니다.
  • page : 요청할 페이지 번호
  • size : 한 페이지 당 조회 할 갯수 (default : 20)
  • sort : Sorting에 대한 값 설정하는 파라미터로, 기본적으로 오름차순이다. 표기는 정렬한 필드명,정렬기준 ex) createdDate,desc
제가 사용한 예제에서는 default size를 10, sort 파라미터를 sortBy, 구분자를 '-' 로 커스터마이징하여서 사용하였습니다. pageable 를 커스터마이징하는 방법은 추후에 포스팅하도록 하겠습니다.  
- size : 3 , page = 2 로 호출한 경우

{
    "content": [
        {
            "createdDate": "2020-03-13T10:05:46",
            "updatedDate": "2020-03-13T07:05:46",
            "id": 4,
            "title": "4번 게시글",
            "content": "안녕하세요",
            "writer": "남산돈가스"
        },
        {
            "createdDate": "2020-03-13T11:05:46",
            "updatedDate": "2020-03-13T07:05:46",
            "id": 5,
            "title": "5번 게시글",
            "content": "안녕하세요",
            "writer": "남산타워"
        },
        {
            "createdDate": "2020-03-13T12:05:46",
            "updatedDate": "2020-03-13T07:05:46",
            "id": 6,
            "title": "6번 게시글",
            "content": "안녕하세요",
            "writer": "남산돈가스"
        }
    ],
    "pageable": {
        "sort": {
            "sorted": false,
            "unsorted": true,
            "empty": true
        },
        "offset": 3,
        "pageSize": 3,
        "pageNumber": 1,
        "paged": true,
        "unpaged": false
    },
    "totalElements": 18,
    "last": false,
    "totalPages": 6,
    "size": 3,
    "number": 1,
    "numberOfElements": 3,
    "sort": {
        "sorted": false,
        "unsorted": true,
        "empty": true
    },
    "first": false,
    "empty": false
}


- size : 5, page :4, sorting : 생성일자 기준 내림차순 정렬


{
    "content": [
        {
            "createdDate": "2020-03-13T09:05:46",
            "updatedDate": "2020-03-13T07:05:46",
            "id": 3,
            "title": "3번 게시글",
            "content": "안녕하세요",
            "writer": "명동교자\n"
        },
        {
            "createdDate": "2020-03-13T08:05:46",
            "updatedDate": "2020-03-13T07:05:46",
            "id": 2,
            "title": "2번 게시글",
            "content": "안녕하세요",
            "writer": "남산돈가스"
        },
        {
            "createdDate": "2020-03-13T07:05:46",
            "updatedDate": "2020-03-13T07:05:46",
            "id": 1,
            "title": "1번 게시글",
            "content": "안녕하세요",
            "writer": "남산돈가스"
        }
    ],
    "pageable": {
        "sort": {
            "sorted": true,
            "unsorted": false,
            "empty": false
        },
        "offset": 15,
        "pageSize": 5,
        "pageNumber": 3,
        "paged": true,
        "unpaged": false
    },
    "totalElements": 18,
    "last": true,
    "totalPages": 4,
    "size": 5,
    "number": 3,
    "numberOfElements": 3,
    "sort": {
        "sorted": true,
        "unsorted": false,
        "empty": false
    },
    "first": false,
    "empty": false
}

총 18개 데이터에서 5개씩 4번째 페이지를 요청한 결과 마지막 페이지의 3개의 데이터가 조회되었으며, 정렬조건으로 생성일자 내림차순으로 요청하여 최신으로 생성 된 세개의 결과가 조회되었습니다.

결론적으로, 이렇게 Spring Data JPA와 Pageable을 유연하게 사용하시면 매번 Pagination 이나 Sorting에 대한 부담 및 고민을 쉽게 해결하실 수 있으실 것이라고 생각됩니다.

감사합니다.

댓글

주간 인기글

[정보] 인스타그램은 당신의 소리를 '듣고' 있을 수도 있습니다

[Angular] 모델, 값이 바뀌었는데 화면 template 이 업데이트 되지 않을 때 조치 팁

[AWS] Lambda + API GateWay를 이용해 간단한 RESTful API 만들기 #1

[AWS] Lambda + API GateWay를 이용해 간단한 RESTful API 만들기 #2

안드로이드에서 당겨서 새로고침(SwipeRefreshLayout) 쉽게 구현하기