[Spring/Kotlin] 페이징 기법

트러블 슛돌이·2023년 7월 25일

페이징이란?

페이징은 사용자에게 데이터를 제공할 때, 전체 데이터 중의 일부르 보여주는 방식이다
예를 들어서, 사용자의 쪽지 목록이 100개라고 할 때, 한 페이지에 쪽지 100개를 전부 보여주면 로딩 속도가 느려지는 단점이 있을 수 있다
페이징은 이러한 문제점을 해결할 수 있으며 검색 기능을 통해 원하는 데이터를 조회할 수 있다

페이징 관련 클래스 만들기

페이징과 검색을 처리하기 위해서는 몇 가지 파라미터가 필요하다

  1. page : 현재 페이지 번호를 의미
  2. recordSize : 한 페이지 당 보여줄 데이터의 갯수
  3. pageSize : 화면 상에서 보여줄 페이지의 크기를 의미, 예를 들어 size를 5로 지정하면 5페이지 단위로 데이터가 보여진다
data class PagingDto(
    var page: Int = 1, //현재 페이지 번호
    var recordSize: Int = 10, //페이지 당 출력할 데이터 개수
    var pageSize: Int = 10, //화면에 보여줄 페이지 사이즈
) {
    val offset: Int
        get() = (page - 1) * recordSize
}

객체가 생성되는 시점에 현재 페이지 번호를 1로, 페이지 당 출력할 데이터 개수를 10으로 지정해준다
offset의 get메서드를 통해 쿼리문 안의 LIMIT 구문의 시작부분을 지정해 준다

Mapper, XML Mapper 작성

매퍼를 작성해준다

@Mapper
@Repository
interface ProfileMapper {
	//작성 글을 불러올 때 페이징 처리 된 상태로 보여주도록 하는 메서드
    fun findAllBoardByIdWithPaging(userId: Int, pagingDto: PagingDto): List<BoardDTO>

	//userId에 일치하는 유저의 게시글 갯수를 반환하는 메서드 
	fun getBoardCount(userId: Int): Int
}

각 매퍼에 매치되는 xml mapper 또한 작성한다

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.tovelop.maphant.mapper.ProfileMapper">

    <select id="findAllBoardByIdWithPaging" resultType="BoardDTO">
        SELECT categoryId,
               userId,
               postId,
               type,
               title,
               body,
               state,
               isAnonnymous,
               creatAt,
               modifiedAt
        FROM board
        WHERE userId = #{user_id}
        ORDER BY created_at
        LIMIT #{params.recordSize} OFFSET #{params.offset}
    </select>

    <select id="getBoardCount">
        SELECT COUNT(*)
        FROM board
        WHERE userId = #{user_id}
    </select>

</mapper>

LIMIT문이란?

리미트 구문은 SELECT 쿼리와 함께 사용되며 테이블의 데이터를 조회 시 한계를 지정할 수 있다

findAllBoardByIdWithPaging 쿼리의 params.offset은 pagingDto의 get()메서드가 리턴하는 (page-1)*recordSize를 계산한 값이다

예를 들어 page를 1로 recordSize를 10으로 가정한다면 (1-1)*10 = 0이다

즉, 현재 페이지 번호가 1이라면 쿼리는 LIMIT 0, 10 으로 실행되어 0부터 10까지의 데이터를 보여준다

pagination 클래스 작성

페이지네이션이란 웹 화면에서 페이지의 번호를 출력하는 기능이다
위에서 작성한 pagingDto 멤버 변수를 이용해 페이지 정보를 계산할 수 있다

data class Pagination(
    var totalRecordCount: Int = 0,        // 전체 데이터 수
    var totalPageCount: Int = 0,          // 전체 페이지 수
    var startPage: Int = 0,               // 첫 페이지 번호
    var endPage: Int = 0,                 // 끝 페이지 번호
    var limitStart: Int = 0,              // LIMIT 시작 위치
    var existPrevPage: Boolean = false,   // 이전 페이지 존재 여부
    var existNextPage: Boolean = false    // 다음 페이지 존재 여부
) {
    constructor(totalRecordCount: Int, params: PagingDto) : this() {
        if (totalRecordCount > 0) {
            this.totalRecordCount = totalRecordCount
            calculation(params)
        }
    }

    private fun calculation(params: PagingDto) {
        // 전체 페이지 수 계산
        totalPageCount = ((totalRecordCount - 1) / params.recordSize) + 1

        // 현재 페이지 번호가 전체 페이지 수보다 큰 경우, 현재 페이지 번호에 전체 페이지 수 저장
        if (params.page > totalPageCount) {
            params.page = totalPageCount
        }

        // 첫 페이지 번호 계산
        startPage = ((params.page - 1) / params.pageSize) * params.pageSize + 1

        // 끝 페이지 번호 계산
        endPage = startPage + params.pageSize - 1

        // 끝 페이지가 전체 페이지 수보다 큰 경우, 끝 페이지에 전체 페이지 수 저장
        if (endPage > totalPageCount) {
            endPage = totalPageCount
        }

        // LIMIT 시작 위치 계산
        limitStart = (params.page - 1) * params.recordSize

        // 이전 페이지 존재 여부 확인
        existPrevPage = startPage != 1

        // 다음 페이지 존재 여부 확인
        existNextPage = (endPage * params.recordSize) < totalRecordCount
    }
}
  1. totalRecordCount : COUNT(*) 쿼리 실행 결과로 전체 개시글 개수를 의미한다
  2. totalPageCount : 웹에 나타낼 전체 페이지 갯수
  3. startPage : 현재 페이지네이션의 첫 페이지
  4. endPage : 현재 페이지네이션의 끝 페이지
  5. limitStart : 현재 find쿼리의 LIMIT 구문에 사용되는 offset과 동일한 기능을 하는 변수
  6. existPrevPage : 이전 페이지의 존재 여부 확인
  7. existNextPage : 다음 페이지의 존재 여부 확인
  8. 생성자 : 객체 생성 시점에 페이지 정보를 계산
  9. calculation() : 게시글에 데이터가 있는 경우에만 실행되는 로직으로 생성자를 통해 산출 된 전체 데이터의 개수와, pagingDto 객체를 이용하여 클래스의 각 멤버 변수에 값을 세팅한다

페이징 전용 응답 클래스 작성

화면에 페이지 번호를 보여주는 것은 html단에서 이루어진다. 이 때 html에서는 리스트의 데이터와 pagination 객체 모두를 필요로 한다

getBoardList의 findAllBoardByIdwithPaging()의 리턴 값에 pagination객체와 리스트의 데이터 모두 컨트롤러로 보내줄 수 없기 때문에 이를 위한 새로운 응답용 클래스를 작성한다

open class PagingResponse<T>(
    val list: List<T> = mutableListOf(),
    val pagination: Pagination?
)
  1. list에서 제네릭을 통해 다양한 타입의 객체를 데이터로 받을 수 있다
  2. pagination 변수에 계산된 페이지 정보를 저장할 수 있다

서비스 작성

fun getBoardsList(userId: Int, params: PagingDto):PagingResponse<BoardDTO>{
        //getBoardCount xml 구현
        val count = profileMapper.getBoardCount(userId);

        if(count < 1) {
            return PagingResponse(Collections.emptyList(),null);
        }

        //findAllBoardByIdwithPaging xml 구현
        val pagination = Pagination(count,params);
        val boards = profileMapper.findAllBoardByIdWithPaging(userId,params);

        return PagingResponse(boards,pagination);
    }

컨트롤러 작성

getBoardList가 뷰로 전달하는 데이터를 pagingDto로 변경해준다

@GetMapping("/board")
    fun getBoardList(@ModelAttribute pagingDto: PagingDto): ResponseEntity<Response<PagingResponse<BoardDTO>>>{
        val auth = SecurityContextHolder.getContext().authentication!! as TokenAuthToken
        val userId:Int = auth.getUserData().id!!

        return ResponseEntity.ok().body(Response.success(profileService.getBoardsList(userId,pagingDto)));
    }

@ModelAttribute

위에서 작성한 컨트롤러를 보면 파라미터로 받는 pagingDto 앞에 @ModelAttribute가 붙어있는 것을 알 수 있다

그래서 이에 대해서도 간단하게 다뤄보고자 한다

이 어트리뷰트는 메소드 레벨, 메소드의 파라미터 두 곳에서 사용가능하다

이는 사용자 요청시 전달하는 값을 오브젝트 형태로 매핑해주는 어노테이션이다

예를 들엇 위에서 작성한 컨트롤러에서 여러 인스턴스 변수를 담고있는 pagingDto를 파라미터로 사용하면 각각의 값들이 핸들러의 pagingDto 객체로 바인딩된다

단, Setter가 존재해야한다

0개의 댓글