스프링 부트 Pageable - seupeuling buteu Pageable

스프링 부트 Pageable - seupeuling buteu Pageable

해당 글은 배워보자 Spring Data JPA 시리즈 입니다.
해당 시리즈의 내용이 이어지는 형태이므로 글의 내용 중에 생략되는 말들이 있을 수 있으니, 자세한 사항은 아래 링크를 참고해주세요!

  • Spring Data JPA의 기본과 프로젝트 생성 :: 시리즈 학습 환경 준비
    • JPA의 기본과 Spring Data JPA
    • Springboot project 에서 JPA 설정하기
  • JPA의 기본 어노테이션들 :: JPA의 시작과 동시에 끝
    • 엔티티와 테이블 매핑
    • 필드와 컬럼 매핑
  • 매핑 테이블과 연관관계 매핑하기 :: RDB의 꽃, 연관 관계
    • 연관관계란?, 외래 키란?, 매핑 테이블이란?
    • 일대일, 일대다, 다대일 연관관계
  • 공통 인터페이스 기능 :: 어떻게 Data JPA 는 동작할까?
    • 단건 조회 반환 타입
    • 컬렉션 조회 반환 타입
  • 사용자 정의 쿼리 이용하는 방법 :: 내가 원하는 쿼리를 JPA 에서 만들어보자!
    • 메서드 이름으로 쿼리 생성하기
    • @Query 를 이용하여 메서드에 정적 쿼리 작성하기
  • 페이징과 정렬 :: 게시판과 같은 페이지가 있는 서비스에서 빛을 발하는 JPA 페이징!
    • Data JPA의 페이징과 정렬
    • Web MVC 에서 JPA 페이징과 정렬
  • Auditing :: 모든 요청과 응답에 누가, 언제 접근했는지 하나의 엔티티로 관리하자.
    • 순수 JPA의 Auditing
    • Spring Data JPA의 Auditing
  • 배워보자 Spring Data JPA 시리즈를 마치며...
    • 시리즈를 마치며 느낀점
    • 내가 정보를 얻은 곳
    • 해당 시리즈를 완주하셨나요?

게시판이나 댓글, 블로그를 개발할 때 페이징은 아주 중요한 역할을 한다.

페이징은 많은 정보, 이를테면 게시판에 존재하는 수백 수천개의 게시글과 같은 정보들을 페이지로 나눠 효과적으로 정보를 제공하게 하는 역할을 한다.

스프링 부트 Pageable - seupeuling buteu Pageable
페이징

이러한 페이징을 개발하기 위해서는 page 관련 쿼리를 파라미터로 받아서 직접 처리하는 방법이 있었지만 JPA 에서 또 Spring Data 프로젝트에서는 효과적으로 페이징을 처리할 수 있게 방법을 제공한다.

Spring Data JPA 에서의 페이징과 정렬

Spring Data JPA 에서는 앞서 말 했듯 페이징과 정렬에 아주 강력한 방법을 제공한다.

지난 시간에 봤던 JpaRepository 인터페이스의 상속 다이어그램을 살펴보자.

스프링 부트 Pageable - seupeuling buteu Pageable

JpaRepository 의 부모 인터페이스인 PagingAndSortingRepository 에서 페이징과 소팅이라는 기능을 제공한다.

스프링 부트 Pageable - seupeuling buteu Pageable

findAll() 메서드의 반환 타입과 파라미터를 보면 다음과 같은 것들이 존재한다.

  • org.springframework.data.domain.Pageable
    • 페이징을 제공하는 중요한 인터페이스이다.
  • org.springframework.data.domain.Page
    • 페이징의 findAll() 의 기본적인 반환 메서드로 여러 반환 타입 중 하나이다.

결국 우리는 JpaRepository<> 를 사용할 때, findAll() 메서드를 Pageable 인터페이스로 파라미터를 넘기면 페이징을 사용할 수 있게된다.

한 번 사용해보자.

이번에는 controller 를 만들어서 사용해보자.

@RestController
public class UserController {

    private final UserRepository userRepository;

    public UserController(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @GetMapping("/users")
    public Page<User> getAllUsers() {
        PageRequest pageRequest = PageRequest.of(0, 5);
        return userRepository.findAll(pageRequest);
    }

    @PostConstruct
    public void initializing() {
        for (int i = 0; i < 100; i++) {
            User user = User.builder()
                    .username("User " + i)
                    .address("Korea")
                    .age(i)
                    .build();
            userRepository.save(user);
        }
    }
}

getAllUsers() 메서드에 보면 PageRequest 객체가 존재한다.

PageRequest 객체는 Pageable 인터페이스를 상속받는다.

스프링 부트 Pageable - seupeuling buteu Pageable

쉽게 Paging 을 위한 정보를 넘길 수 있는데, 이 정보에는 정렬 정보, 페이지 offset, page와 같은 정보가 담겨있다.

원래라면 기본적으로 데이터가 DB에 저장되어 있어야한다.
하지만 우리는 hibernate 전략을 create 로 잡았기 때문에 DB에 저장하는 것이 힘들다.
물론 hibernate.ddl-auto 전략을 update 로 잡는다면 DB에 저장할 수 있지만 페이징을 테스트할 100개의 데이터를 다 넣는 것 보다 Spring 에서 제공하는 초기화 메서드를 이용해서 초기화를 할 것이다.

initializing() 을 이용해서 100명의 데이터를 추가해주고 api 호출을 해보자.

api 호출을 브라우저에서 직접 url 로 해도 좋고 curl 을 해도 좋지만 나는 Postman을 이용할 것이다.

결과를 봐보자.

"content":[
{
    "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, // 해당 페이지에 첫 번째 원소의 수
        "paged": true,
        "unpaged": false
    },
    "totalPages": 20, // 페이지로 제공되는 총 페이지 수
    "totalElements": 100, // 모든 페이지에 존재하는 총 원소 수
    "last": false,
    "number": 0,
    "sort": {
        "sorted": false,
        "unsorted": true,
        "empty": true
    },
    "size": 5,
    "numberOfElements": 5,
    "first": true,
    "empty": false
}

이와 같이 Paging 이 적용된 응답을 받을 수 있다.

content 아래에 있는 데이터들이 paging 과 관련된 정보들이다.

멋지다.

하지만 더 멋진 것은 쿼리 메서드 기능에서도 제공한다는 것이다.

쿼리 메서드에서 페이징 사용하기

public interface UserRepository extends JpaRepository<User, Long> {
    Page<User> findByAddress(String address, Pageable pageable);
}

이렇게 사용자의 주소로 조회하는 쿼리 메서드를 만들고 두 번째 파라미터로 Pageable 을 넘겨주면 된다.

그리고 컨트롤러에 가서 다시 PageRequest 를 만들어주자.

이번에는 더 나아가 쿼리 파라미터로 넘어온 값을 페이지 정보로 만들어 볼 것이다.

@RestController
public class UserController {

    private final UserRepository userRepository;

    public UserController(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @GetMapping("/users")
    public Page<User> getAllUserWithPageByQueryMethod(@RequestParam("page") Integer page, @RequestParam("size") Integer size) {
        PageRequest pageRequest = PageRequest.of(page, size);
        return userRepository.findByAddress("Korea", pageRequest);
    }


    @PostConstruct
    public void initializing() {
        for (int i = 0; i < 100; i++) {
            User user = User.builder()
                    .username("User " + i)
                    .address("Korea")
                    .age(i)
                    .build();
            userRepository.save(user);
        }
    }
}

이렇게 만들고 http://localhost:8080/users/?page=3&size=4 로 요청을 보내면 다음과 같은 결과가 출력된다.

{
  "content": [
    { "id": 13, "username": "User 12", "address": "Korea", "age": 12 },
    { "id": 14, "username": "User 13", "address": "Korea", "age": 13 },
    { "id": 15, "username": "User 14", "address": "Korea", "age": 14 },
    { "id": 16, "username": "User 15", "address": "Korea", "age": 15 }
  ],
  "pageable": {
    "sort": { "sorted": false, "unsorted": true, "empty": true },
    "pageNumber": 3,
    "pageSize": 4,
    "offset": 12,
    "paged": true,
    "unpaged": false
  },
  "totalPages": 25,
  "totalElements": 100,
  "last": false,
  "numberOfElements": 4,
  "number": 3,
  "sort": { "sorted": false, "unsorted": true, "empty": true },
  "size": 4,
  "first": false,
  "empty": false
}

반환 타입에 따른 페이징 결과

Spring Data JPA 에는 반환 타입에 따라서 각기 다른 결과를 제공한다.

  1. Page<T> 타입
  2. Slice<T> 타입
  3. List<T> 타입

각자 다른 결과를 반환해준다.

Page<T> 타입

Page<T> 타입을 반환 타입으로 받게 된다면 offset과 totalPage 를 이용하여 서비스를 제공할 수 있게된다.

Page<T> 는 일반적인 게시판 형태의 페이징에서 사용된다.

스프링 부트 Pageable - seupeuling buteu Pageable

여기서 중요한 정보는 총 페이지 수 이다.

그 정보를 포함하여 반환한다.

Page<T> 타입은 count 쿼리를 포함하는 페이징으로 카운트 쿼리가 자동으로 생성되어 함께 나간다.

Slice<T> 타입

Slice<T> 타입을 반환 타입으로 받게 된다면 더보기 형태의 페이징에서 사용된다.

스프링 부트 Pageable - seupeuling buteu Pageable

응답을 살펴보자.

{
  "content": [
    { "id": 13, "username": "User 12", "address": "Korea", "age": 12 },
    { "id": 14, "username": "User 13", "address": "Korea", "age": 13 },
    { "id": 15, "username": "User 14", "address": "Korea", "age": 14 },
    { "id": 16, "username": "User 15", "address": "Korea", "age": 15 }
  ],
  "pageable": {
    "sort": { "sorted": false, "unsorted": true, "empty": true },
    "pageNumber": 3,
    "pageSize": 4,
    "offset": 12,
    "paged": true,
    "unpaged": false
  },
  "number": 3,
  "numberOfElements": 4,
  "first": false,
  "last": false,
  "size": 4,
  "sort": { "sorted": false, "unsorted": true, "empty": true },
  "empty": false
}

Page<T> 타입의 반환에 없는 것들이 존재한다.


number과 numberOfElements 그리고 Page<T> 에 존재하던 totalPages, totalElements 가 없어졌다.

Slice<T> 타입은 추가 count 쿼리 없이 다음 페이지 확인 가능하다. 내부적으로 limit + 1 조회를 해서 totalCount 쿼리가 나가지 않아서 성능상 조금 이점을 볼 수도 있다.

List<T> 타입

@GetMapping("/users")
public List<User> getAllUsers(Pageable pageable) {
  return userRepository.findAll(pageable);
}

List 반환 타입은 가장 기본적인 방법으로 count 쿼리 없이 결과만 반환한다.

Spring Web MVC 에서 더 편하게 페이징하기

Spring Data JPA의 페이징과 정렬 기능보다 훨씬 간편하게 MVC 에서 사용할 수 있게 한다.

즉, 다음과 같이 사용자가 정의한 파라미터에 따라서도 페이징이 가능하다는 소리이다.

@GetMapping("/users")
public Page<User> getAllUsers(Pageable pageable) {
    return userRepository.findAll(pageable);
}

컨트롤러에서 @GetMapping 에 파리미터로 Pageable 을 추가하면 된다.

그럼 페이징 관련 쿼리가 나온다.

Springboot 내부에서 url 파라미터가 컨트롤러에 바인딩이 될 때, Pageable이 존재하면 PageRequest 객체를 생성한다.

해당 객체에서 역시 정렬도 제공하는데, url을 다음과 같이 치면 정렬과 페이징이 동시에 수행되게 할 수 있다.

  • http://localhost:8080/members?page=0
    • 0번 페이지 부터 20개 조회한다.
      • default 가 20개로 default를 수정하는 방법도 존재한다.
  • http://localhost:8080/members?page=0&size=5
    • 0번 페이지부터 5개 조회한다.
  • http://localhost:8080/members?page=0&size=5&sort=id.desc
    • 0번 페이지부터 5개 조회 하는데, id의 역순으로 조회한다.

이렇게 오늘은 JPA 페이징에 대한 기본을 알아보았다.

내일은 우리의 마지막 여정인 JPA Auditing 에 대해서 알아볼 것이다!