본문 바로가기
JPA

[JPA] 페이징 처리, Paging (Pagination, Infinite Scroll)

by codeyaki 2023. 6. 21.
반응형

페이징 처리

페이지네이션? 무한 스크롤?

페이징 처리는 한 번에 모든 데이터를 가져오면 많은 시간이 걸리기 때문에 단위를 나눠서 데이터를 가져올 수 있도록 단편화시키는 것을 의미한다.

구글만 보더라도 아래와 같이 페이지네이션을 처리해 두었다.

위와 같이 페이지를 나누어 사용자가 골라 접근할 수 있도록 만든 것을 페이지네이션이라고 부른다.

웹페이지를 이용할 때 흔하게 볼 수 있는 모습이다.

 

반면 요즘에는 SNS 같은 곳을 보면 이러한 숫자방식의 페이지네이션이 아닌 스크롤을 모두 내리면 추가적인 콘텐츠를 불러오는 방식의 무한 스크롤(infinite scroll) 방식도 많이 사용한다. 하지만 이러한 방식의 차이는 어떻게 보여주냐의 차이일 뿐 내부적인 로직은 거의 흡사하다.

기본적인 동작 원리는 조회할 때 얼마큼 조회할지(size), 어디부터 조회하기 시작할지(offset)을 정해서 서버에 요청하면 서버가 해당 정보를 바탕으로 데이터를 전달하는 방식을 사용한다.

 

JPA에서 페이징 처리 해보기

프로젝트의 구성은 이러하다. 여기서는 UI를 신경 쓰지 않고 프론트엔드에서 데이터를 사용하기 위해서 rest api로 조회할 때를 가정하겠다.

  • java 17
  • Spring boot 3.1
  • gradle 7.6.1
  • spring web
  • lombok
  • postgresql
  • jpa

build.gradle

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    runtimeOnly 'org.postgresql:postgresql'
}

 

다음으로 서버 설정이다. resources/application.yml

server:
  port: 8080
spring:
  datasource:
    driver-class-name: org.postgresql.Driver
    url: jdbc:postgresql://localhost:5432/postgres
    username: postgres
    password: mypassword
  jpa:
    hibernate:
      ddl-auto: update
  • ddl-auto를 update로 한 이유는 데이터베이스의 테이블을 따로 만드는 작업 없이 하기 위해서 사용하였다. 

 

먼저 페이징처리를 하지 않았을 때를 살펴보겠다.

Entity와 Repository를 만들어준다

package com.ume.jpapaging.model.entity;

import jakarta.persistence.*;
import lombok.*;


// lombok
@Setter
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@ToString
// jpa
@Entity
@Table(name = "TBL_CONTENT")

public class Content {
    @Id
    @Column(name = "no", nullable = false)
    private Integer no;

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

    @Column(name = "detail")
    private String detail;
}
package com.ume.jpapaging.model.respository;

import com.ume.jpapaging.model.entity.Content;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface ContentRepository extends JpaRepository<Content, Integer> {
}

 

또한 controller와 service를 이어서 만들어준다. ResponseDTO와 ContentDTO는 응답을 위해서 만들어 주었다.

package com.ume.jpapaging.controller;

import com.ume.jpapaging.model.dto.ContentDTO;
import com.ume.jpapaging.model.dto.ResponseDTO;
import com.ume.jpapaging.model.service.ContentService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("")
@RequiredArgsConstructor
public class ContentController {

    private final ContentService contentService;

    @GetMapping("contents")
    public ResponseEntity<ResponseDTO> getContentsList(){

        List<ContentDTO> contentList = contentService.getContentList();
        ResponseDTO res = ResponseDTO.builder()
                .result("성공적으로 조회했습니다.")
                .data(contentList)
                .build();

        return ResponseEntity.ok().body(res);

    }
}

 

package com.ume.jpapaging.model.dto;


import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@Builder
public class ResponseDTO {
    private String result;
    private Object data;
}
package com.ume.jpapaging.model.dto;

import com.ume.jpapaging.model.entity.Content;
import lombok.*;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Builder
public class ContentDTO {

    private String title;
    private String detail;

    public static ContentDTO of(Content content) {
        return ContentDTO.builder()
                .title(content.getTitle())
                .detail(content.getDetail())
                .build();
    }
}
package com.ume.jpapaging.model.service;

import com.ume.jpapaging.model.dto.ContentDTO;
import com.ume.jpapaging.model.entity.Content;
import com.ume.jpapaging.model.respository.ContentRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@RequiredArgsConstructor
public class ContentService {
    private final ContentRepository contentRepository;

    public List<ContentDTO> getContentList(){

        List<Content> allContent = contentRepository.findAll();

        List<ContentDTO> res = allContent.stream().map(ContentDTO::of).toList();
        return res;
    }
}

기본적인 controller - service - repository - entity 구조로 작성한 코드이다. 

실행한 뒤 GET /contents을 호출해 보면

{"result":"성공적으로 조회했습니다.","data":[{"title":"test title6","detail":"test detail"},{"title":"test title5","detail":"test detail"},{"title":"test title3","detail":"test detail"},{"title":"test title8","detail":"test detail"},{"title":"test title6","detail":"test detail"},{"title":"test title8304","detail":"test detail"},{"title":"test title1657","detail":"test detail"},{"title":"test title4376","detail":"test detail"},{"title":"test title3734","detail":"test detail"},{"title":"test title1873","detail":"test detail"},{"title":"test title6626","detail":"test detail"},{"title":"test title9361","detail":"test detail"},{"title":"test title4575","detail":"test detail"},{"title":"test title4384","detail":"test detail"},{"title":"test title9883","detail":"test detail"},{"title":"test title2096","detail":"test detail"},{"title":"test title8978","detail":"test detail"},{"title":"test title4015","detail":"test detail"},{"title":"test title5688","detail":"test detail"},{"title":"test title8757","detail":"test detail"},{"title":"test title5222","detail":"test detail"},{"title":"test title5295","detail":"test detail"},{"title":"test title2529","detail":"test detail"},{"title":"test title5387","detail":"test detail"},{"title":"test title3152","detail":"test detail"},{"title":"test title8387","detail":"test detail"},{"title":"test title2729","detail":"test detail"},{"title":"test title219","detail":"test detail"},{"title":"test title9948","detail":"test detail"},{"title":"test title6413","detail":"test detail"},{"title":"test title7679","detail":"test detail"},{"title":"test title8442","detail":"test detail"},{"title":"test title1478","detail":"test detail"},{"title":"test title6789","detail":"test detail"},{"title":"test title2560","detail":"test detail"},{"title":"test title1371","detail":"test detail"},{"title":"test title7494","detail":"test detail"},{"title":"test title3487","detail":"test detail"},{"title":"test title2157","detail":"test detail"},{"title":"test title2731","detail":"test detail"},{"title":"test title4768","detail":"test detail"},{"title":"test title5701","detail":"test detail"},{"title":"test title6960","detail":"test detail"},{"title":"test title5446","detail":"test detail"},{"title":"test title7835","detail":"test detail"},{"title":"test title1343","detail":"test detail"},{"title":"test title5282","detail":"test detail"},{"title":"test title4212","detail":"test detail"},{"title":"test title296","detail":"test detail"},{"title":"test title3878","detail":"test detail"},{"title":"test title4985","detail":"test detail"},{"title":"test title8360","detail":"test detail"},{"title":"test title6163","detail":"test detail"},{"title":"test title3091","detail":"test detail"},{"title":"test title1820","detail":"test detail"},{"title":"test title4218","detail":"test detail"},{"title":"test title3581","detail":"test detail"},{"title":"test title4543","detail":"test detail"},{"title":"test title9511","detail":"test detail"},{"title":"test title7663","detail":"test detail"},{"title":"test title5074","detail":"test detail"},{"title":"test title3430","detail":"test detail"},{"title":"test title8719","detail":"test detail"},{"title":"test title6966","detail":"test detail"},{"title":"test title8250","detail":"test detail"},{"title":"test title3644","detail":"test detail"},{"title":"test title9231","detail":"test detail"},{"title":"test title3489","detail":"test detail"},{"title":"test title6091","detail":"test detail"},{"title":"test title3660","detail":"test detail"},{"title":"test title8639","detail":"test detail"},{"title":"test title9918","detail":"test detail"},{"title":"test title7051","detail":"test detail"},{"title":"test title9001","detail":"test detail"},{"title":"test title6694","detail":"test detail"},{"title":"test title418","detail":"test detail"},{"title":"test title1203","detail":"test detail"},{"title":"test title9987","detail":"test detail"},{"title":"test title1046","detail":"test detail"},{"title":"test title2705","detail":"test detail"},{"title":"test title31","detail":"test detail"},{"title":"test title1305","detail":"test detail"},{"title":"test title9190","detail":"test detail"},{"title":"test title3356","detail":"test detail"},{"title":"test title7778","detail":"test detail"},{"title":"test title1013","detail":"test detail"},{"title":"test title5262","detail":"test detail"},{"title":"test title8647","detail":"test detail"},{"title":"test title2758","detail":"test detail"},{"title":"test title1512","detail":"test detail"},{"title":"test title4091","detail":"test detail"},{"title":"test title7589","detail":"test detail"},{"title":"test title6641","detail":"test detail"},{"title":"test title4818","detail":"test detail"},{"title":"test title6898","detail":"test detail"},{"title":"test title1955","detail":"test detail"},{"title":"test title4787","detail":"test detail"},{"title":"test title9038","detail":"test detail"},{"title":"test title3553","detail":"test detail"},{"title":"test title575","detail":"test detail"},{"title":"test title732","detail":"test detail"},{"title":"test title360","detail":"test detail"},{"title":"test title561","detail":"test detail"},{"title":"test title7859","detail":"test detail"},{"title":"test title1786","detail":"test detail"},{"title":"test title5356","detail":"test detail"},{"title":"test title4984","detail":"test detail"},{"title":"test title1921","detail":"test detail"},{"title":"test title3824","detail":"test detail"},{"title":"test title3324","detail":"test detail"},{"title":"test title4027","detail":"test detail"},{"title":"test title8764","detail":"test detail"},{"title":"test title707","detail":"test detail"},{"title":"test title5166","detail":"test detail"},{"title":"test title6480","detail":"test detail"},{"title":"test title4688","detail":"test detail"},{"title":"test title8542","detail":"test detail"},{"title":"test title8288","detail":"test detail"},{"title":"test title65","detail":"test detail"},{"title":"test title1088","detail":"test detail"},{"title":"test title3365","detail":"test detail"},{"title":"test title3929","detail":"test detail"},{"title":"test title1357","detail":"test detail"},{"title":"test title6970","detail":"test detail"},{"title":"test title5990","detail":"test detail"},{"title":"test title1789","detail":"test detail"},{"title":"test title5768","detail":"test detail"},{"title":"test title1529","detail":"test detail"},{"title":"test title955","detail":"test detail"},{"title":"test title8956","detail":"test detail"},{"title":"test title3587","detail":"test detail"},{"title":"test title4983","detail":"test detail"},{"title":"test title5276","detail":"test detail"},{"title":"test title1367","detail":"test detail"},{"title":"test title7814","detail":"test detail"}]}

다음과 같은 응답을 받을 수 있다. 현재 데이터베이스에 135개가 들어있다.

만약 135개가 아니라 콘텐츠가 1350개나 13500개와 같이 많은 양이 들어있다면 데이터를 조회하는데 많은 시간이 사용될 것이다. 자기가 보지도 않은 대다수의 데이터를 위해서 말이다...

이를 해결하기 위해서 데이터를 나눠서 볼 수 있도록 페이지를 나눠서 조회해 보도록 하겠다.

 

 

Pageable

Spring data에는 이러한 페이징 처리를 위해서 Pageable를 지원해 준다. controller에 파라미터에 Pageable을 넣으면 현재 위치, 크기, 정렬기준을 자동으로 파싱 해준다.

예를 들어 url에 http://localhost:8080/contents?page=2&size=30&sort=title,desc 와 같이 넣으면 

Page request [number: 2, size 30, sort: title: DESC]가 자동으로 생성된다. 

number는 현재 offset, size는 한 번에 조회할 크기, sort는 정렬 기준이다. 만약 오름차순으로 하고 싶다면 asc, 내림차순은 desc이다.

url에 아무것도 입력하지 않았을 때 기본값들을 설정해주고 싶다면 @PageableDefault 어노테이션을 달아주면 된다.

package com.ume.jpapaging.controller;

import com.ume.jpapaging.model.dto.ResponseDTO;
import com.ume.jpapaging.model.entity.Content;
import com.ume.jpapaging.model.service.ContentService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("")
@RequiredArgsConstructor
public class ContentController {

    private final ContentService contentService;

    @GetMapping("contents")
    public ResponseEntity<ResponseDTO> getContentsList(@PageableDefault(page = 0, size = 5, sort = "title", direction = Sort.Direction.ASC) Pageable pageable) {
        System.out.println(pageable);

        Page<Content> contentList = contentService.getContentList(pageable);
        ResponseDTO res = ResponseDTO.builder()
                .result("성공적으로 조회했습니다.")
                .data(contentList)
                .build();

        return ResponseEntity.ok().body(res);

    }

}

 

  • getContentsList의 파라미터 부분에 보면 @PageableDefault를 볼 수 있다. 
  • @PageableDefault(page = 1, size = 5, sort = "title", direction = Sort.Direction.ASC)
    • page는 offset을 의미한다. 즉 현재 페이지 위치를 의미합니다. index처럼 0부터 시작한다.
    • size는 한 번에 가져올 페이지의 크기를 의미한다.
    • sort는 정렬할 기준을 정해주면 됩니다. String으로 작성해주어야 한다.
    • direction은 오름차순 (Sort.Direction.ASC) /  내림차순 (Sort.Direction.DESC)을 설정할 수 있다.
  • 마지막으로 서비스 호출 시 pageable을 넘겨준다. 또한 반환 타입을 Page<Content>로 받아야 한다.

 

서비스 또한 변경이 필요하다.

package com.ume.jpapaging.model.service;

import com.ume.jpapaging.model.entity.Content;
import com.ume.jpapaging.model.respository.ContentRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class ContentService {
    private final ContentRepository contentRepository;

    public Page<Content> getContentList(Pageable pageable) {

        Page<Content> pageResult = contentRepository.findAll(pageable);
//        List<ContentDTO> res = pageResult.getContent().stream().map(ContentDTO::of).toList();
        return pageResult;
    }


}
  • JPA 레파지토리에는 Pageable와 Page를 받는 메서드가 오버로딩되어 있습니다. 따라서 파라미터에 넣어주기만 한다면 자동으로 Page<>로 매핑이 되어 반환된다. Page<>에는 다양한 정보가 포함되어 있다.

해당 결과를 실행하면 아래와 같이 응답을 받게 된다.

{
    "result": "성공적으로 조회했습니다.",
    "data": {
        "content": [
            {
                "no": 86,
                "title": "test title1013",
                "detail": "test detail"
            },
            {
                "no": 79,
                "title": "test title1046",
                "detail": "test detail"
            },
            {
                "no": 120,
                "title": "test title1088",
                "detail": "test detail"
            },
            {
                "no": 77,
                "title": "test title1203",
                "detail": "test detail"
            },
            {
                "no": 82,
                "title": "test title1305",
                "detail": "test detail"
            }
        ],
        "pageable": {
            "sort": {
                "empty": false,
                "unsorted": false,
                "sorted": true
            },
            "offset": 0,
            "pageNumber": 0,
            "pageSize": 5,
            "paged": true,
            "unpaged": false
        },
        "last": false,
        "totalPages": 27,
        "totalElements": 135,
        "first": true,
        "size": 5,
        "number": 0,
        "sort": {
            "empty": false,
            "unsorted": false,
            "sorted": true
        },
        "numberOfElements": 5,
        "empty": false
    }
}

 

 

dto로 변환하기 

삽질의 과정

더보기

하지만 이러한 응답은 의도와 다르게 매우 많은 정보를 담고 있어서 문제가 발생할 수 있다. 따라서 DTO로 변환하여 응답해야 한다.

클라이언트가 페이징 처리를 위해서 알아야 할 메타 정보는 현재 페이지, 페이지 크기, 전체 페이지 개수, 전체 콘텐츠 개수이다. 이를 같이 반환할 수 있도록 Paged 된 DTO를 담는 클래스를 만들어 보았다.

package com.ume.jpapaging.model.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.*;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@ToString
@Builder
public class PagedDTO<T> {
    private T content;
    @JsonProperty
    private PageMetaData pageMetaData;

}
  • 제네릭을 이용해서 여러 DTO를 담을 수 있도록 하였다.

pageMetaData에는 다음과 같은 필드를 넣어준다.

package com.ume.jpapaging.model.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.*;


@Setter
@Getter
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class PageMetaData {
    @JsonProperty
    private long size;
    @JsonProperty
    private long totalElements;
    @JsonProperty
    private long totalPages;
    @JsonProperty
    private long number;
}

 

 

적용시키기 위해서 service는 다음과 같이 변경한다.

package com.ume.jpapaging.model.service;

import com.ume.jpapaging.model.dto.ContentDTO;
import com.ume.jpapaging.model.dto.PageMetaData;
import com.ume.jpapaging.model.dto.PagedDTO;
import com.ume.jpapaging.model.entity.Content;
import com.ume.jpapaging.model.respository.ContentRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@RequiredArgsConstructor
public class ContentService {
    private final ContentRepository contentRepository;

    public PagedDTO<List<ContentDTO>> getContentList(Pageable pageable) {

        Page<Content> pageResult = contentRepository.findAll(pageable);

        List<ContentDTO> contentDTOList = pageResult.getContent().stream().map(ContentDTO::of).toList();

        PageMetaData pageMetaData = new PageMetaData(pageResult.getSize(),
                pageResult.getTotalElements(), pageResult.getTotalPages(), pageResult.getNumber());

        PagedDTO<List<ContentDTO>> res = PagedDTO.<List<ContentDTO>>builder()
                .content(contentDTOList).pageMetaData(pageMetaData).build();
        return res;
    }


}
  • 먼저 getContent를 통해서 Content리스트를 얻고 그 리스트를 ContentDTOList로 변경해 주었다.
  • 그 뒤에 현재 페이징 정보를 토대로 pageMetaData를 생성해 주었다.
  • 그 두 개를 합쳐서 PagedDTO에 담아준 뒤 응답하면 된다.

서비스의 응답이 변경되었기 때문에 컨트롤러에서 또한 변경이 이뤄져야 한다.

package com.ume.jpapaging.controller;

import com.ume.jpapaging.model.dto.ContentDTO;
import com.ume.jpapaging.model.dto.PagedDTO;
import com.ume.jpapaging.model.dto.ResponseDTO;
import com.ume.jpapaging.model.service.ContentService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("")
@RequiredArgsConstructor
public class ContentController {

    private final ContentService contentService;

    @GetMapping("contents")
    public ResponseEntity<ResponseDTO> getContentsList(@PageableDefault(page = 0, size = 5, sort = "title", direction = Sort.Direction.ASC) Pageable pageable) {
        System.out.println(pageable);

        PagedDTO<List<ContentDTO>> contentList = contentService.getContentList(pageable);
        ResponseDTO res = ResponseDTO.builder()
                .result("성공적으로 조회했습니다.")
                .data(contentList)
                .build();

        return ResponseEntity.ok().body(res);

    }

}

이렇게 변경하고 다시 요청을 보내보면

{
    "result": "성공적으로 조회했습니다.",
    "data": {
        "content": [
            {
                "title": "test title1820",
                "detail": "test detail"
            },
            {
                "title": "test title1873",
                "detail": "test detail"
            },
            {
                "title": "test title1921",
                "detail": "test detail"
            },
            {
                "title": "test title1955",
                "detail": "test detail"
            },
            {
                "title": "test title2096",
                "detail": "test detail"
            }
        ],
        "pageMetaData": {
            "size": 5,
            "totalElements": 135,
            "totalPages": 27,
            "number": 3
        }
    }
}

으로 필요한 정보만 골라서 보낼 수 있게 되었다.

 


위의 과정이 너무나도 비효율적인것 같아서 데이터베이스에서 가져올 때 dto에 담는 방법이 없을까 알아본 결과

JPQL을 사용해서 쿼리의 결과에 DTO를 매핑시킬 수 있다.

위에 접은 글에는 모든 데이터를 가져온뒤 dto에 변환하는 행위를 했는데 JPQL을 사용하면 데이터베이스에서 조회할 때 dto에 담아줄 수 있어 불필요한 연산을 줄일 수 있다... JPA는 파면 팔수록 대단한 녀석인 것 같다. JPQL도 제대로 학습을 해보아야겠다.

위의 삽질의 흔적은 넘어가고 이제 dto에 담는 방법을 다시 작성하도록 하겠다...

 

먼저 레파지토리를 수정해주어야 한다.

package com.ume.jpapaging.model.respository;

import com.ume.jpapaging.model.dto.ContentDTO;
import com.ume.jpapaging.model.entity.Content;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;

@Repository
public interface ContentRepository extends JpaRepository<Content, Integer> {

    @Query("select new com.ume.jpapaging.model.dto.ContentDTO(c.title, c.detail) from Content c")
    Page<ContentDTO> findAllContentDTO(Pageable pageable);
}

 

  • 레파지토리에 @Query를 추가하여 JPQL을 사용해준다.
  • select절에 dto의 인스턴스를 생성해 주면 된다. (풀 패키지명을 작성해주어야 한다)
  • 페이징 처리는 JPQL을 사용할 때에도 레파지토리가 자동으로 처리해 주기 때문에 Pageable만 넣어주면 된다.

 

서비스문은 이렇게 변경해 주면 된다.

package com.ume.jpapaging.model.service;

import com.ume.jpapaging.model.dto.ContentDTO;
import com.ume.jpapaging.model.respository.ContentRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class ContentService {
    private final ContentRepository contentRepository;

    public Page<ContentDTO> getContentList(Pageable pageable) {
        Page<ContentDTO> allContentDTO = contentRepository.findAllContentDTO(pageable);

        return allContentDTO;
    }
}
  • 레파지토리에서 선언한 query 메서드를 사용해주면 된다.

마찬가지로 Controller 또한 호출 메서드 변경에 따른 리턴타입만 바꿔주면 된다.

package com.ume.jpapaging.controller;

import com.ume.jpapaging.model.dto.ContentDTO;
import com.ume.jpapaging.model.dto.ResponseDTO;
import com.ume.jpapaging.model.service.ContentService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("")
@RequiredArgsConstructor
public class ContentController {

    private final ContentService contentService;

    @GetMapping("contents")
    public ResponseEntity<ResponseDTO> getContentsList(@PageableDefault(page = 0, size = 5, sort = "title", direction = Sort.Direction.ASC) Pageable pageable) {
        Page<ContentDTO> contentList = contentService.getContentList(pageable);
        ResponseDTO res = ResponseDTO.builder()
                .result("성공적으로 조회했습니다.")
                .data(contentList)
                .build();

        return ResponseEntity.ok().body(res);

    }

}

이렇게 효과적으로 코드가 변경되게 된다.

 

실제로 발생한 쿼리를 살펴보면 설렉트문에 필요한 속성만 담아서 쿼리를 요청하는 것을 알 수 있다.

위에 두줄은 변경하기 전 전체를 가져온 뒤 dto로 변환했던 것이고 아래는 JPQL을 이용해서 바로 dto에 매핑하는 것이다. 

또한 두 줄씩 발생하는 이유는 JPA에서 Page로 반환받는 경우 총 페이지 개수, 행의 개수를 함께 알려주기 때문이다.

 

물론 지금은 content에 많은 정보가 있지 않기 때문에 왜 이렇게 까지 해야 하나 싶겠지만 만약 Content에 공개되어서는 안 되는 정보가 섞여 있는 경우 클라이언트에게 그대로 공개하면 문제가 발생할 수 있기 때문에 이렇게 공개해야 하는 정보만 접할 수 있도록 하는 것이 중요하다.

 

만약 전체 코드를 살펴보고 싶다면 https://github.com/5onchangwoo/study/tree/main/backend/jpa-paging에 방문하여 살펴볼 수 있다.

 

GitHub - 5onchangwoo/study: 블로그 주소

블로그 주소. Contribute to 5onchangwoo/study development by creating an account on GitHub.

github.com

 

Pageable

Pageable의 공식 API 문서이다.

https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/domain/Pageable.html

 

Pageable (Spring Data Core 3.1.1 API)

isUnpaged default boolean isUnpaged() Returns whether the current Pageable does not contain pagination information. Returns:

docs.spring.io

  • 페이지에 관련된 여러 정보들을 얻고, 동작시킬 수 있는 메서드들이 많다.
  • 또한 scrollpostion에 대한 정보도 얻을 수 있는 것으로 보인다. 무한 스크롤 방식에서는 이 부분을 사용하여 구현할 수 있는 것으로도 보인다.

Pageable의 기본 설정을 하는 방법에는

1. @PageableDefault 어노테이션을 사용하여 설정

2. application.properties (yml)에 설정

3. CustomPageableConfiguration 클래스 생성하기

순으로 적용된다.

좀 더 자세한 내용은 https://tecoble.techcourse.co.kr/post/2021-08-15-pageable/ 에서 살펴볼 수 있다.

 

Pageable을 이용한 Pagination을 처리하는 다양한 방법

Spring Data JPA에서 Pageable 를 활용한 Pagination 의 개념과 방법을 알아본다.

tecoble.techcourse.co.kr

 

Page<T>

다음으로 Page<T>의 공식 문서 주소이다.

https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/domain/Page.html

 

Page (Spring Data Core 3.1.1 API)

getTotalPages int getTotalPages() Returns the number of total pages. Returns: the number of total pages

docs.spring.io

Page 클래스의 메서드

Page가 상속받고 있는 Slice 클래스의 메서드

Slice가 상속받고 있는 Streamable클래스의 메서드

만약 총페이지의 개수가 필요하다면 Page로 반환해야 한다. getTotalElements와 getTotalPagesr가 Page에 있기 때문이다.

이 두 가지 정보가 필요 없는 "더 보기"나 무한스크롤을 사용하는 방식의 경우 Slice로 반환받는 것이 좋다.
그 이유는 Page로 넘겨받게 되는 경우 한 번의 쿼리가 추가적으로 나가게 된다. 즉 쿼리를 두 번 날린다.


반면 Slice로 반환받게 되면 개수를 구하는 쿼리문이 나가지 않게 된다.

만약 페이징 정보를 제외한 Content 리스트만 얻고 싶다면 List로 반환받으면 바로 받을 수 있다.

 

반황형에 따른 결과는 이러하다

  • Page<T> : 콘텐츠 + 페이징 정보 (+ 전체 요소 개수, 전체 페이지 개수)
  • Slice<T> : 콘텐츠 + 페이징 정보
  • List<T> : 콘텐츠
  • Streamable<T> 형태로 받으면 stream을 사용할 수 있는 타입으로 반환되어 사용할 수 있다.
반응형