Post

[Spring framework & MyBatis] 페이징 처리하기

[Spring framework & MyBatis] 페이징 처리하기

📘 이 글은 『자바 웹 개발 워크북 - 구멍가게 코딩단』 책을 학습하며 제 방식대로 정리한 내용입니다.
개인 학습 목적의 정리이며, 저작권은 원저작자에게 있습니다.

🤔 Intro

  • 웹 페이지를 구성할 때, 여러 리스트를 화면에 묶어서 표현하기 위해서는 반드시 페이징 처리가 필요하다.

  • spring framework + MyBatis를 이용해서 페이징 처리를 어떻게 할 수 있을지 학습해보자.

😀 Start!

계획

  • 우선 페이징 처리를 어떤 방식으로 구현해야 할 지를 생각해보기 위해, 페이지 화면에 어떤 요소가 필요할지를 고민해보자.

  • 해당 페이지는 페이징 실습으로 만든 todoList 페이지이다.

    main

    1. 한 페이지당 10개의 list가 존재한다.
    2. 페이지 바에는 뒤로 이동할 수 있는 Previous와 앞으로 이동할 수 있는 Next가 존재한다.
    3. 페이지 번호는 10개씩 표시된다.
      • 단, 끝 번호의 경우 10개로 나눈 것보다 작으면 나눠진 것 만큼만 나와야 한다.
      • 예를 들면, 전체 페이지가 75개인 경우 8까지만 나와야 한다.

구현

Mapper

  • 우선 목록 페이지를 10개씩 가져와야 하므로 mapper를 구성해서 정보들을 가져와야 한다.
    • mapper를 구성하는 것은 이 프로젝트가 MyBatis 기반이기 때문이다..
  • 페이징 처리를 위한 sql을 작성할때는 다음과 같은 정보가 필요하다.

    • 몇 개씩 가져올 것인지
    • 몇 개를 스킵하고 몇 번부터 가져올 것인지
    • 이를 위해 limit (skip), (fetch)를 사용할 수 있다.
  • 예를 들어 살펴보자.

    • 두 번째 페이지 (11~20)의 경우, skip은 10, fetch는 10이 된다.
    • 다섯 번째 페이지 (41~50)의 경우, skip은 40, fetch는 10이 된다.
  • 이를 적용한 sql문은 다음과 같다. 첫 번째 예시인 두 번째 페이지의 예시문이다.

    1
    
    SELECT * FROM 테이블 ORDER BY DESC limit 10,10;
    
  • 전체 페이지를 쪼개서 보여주는 것이므로, 페이징 처리를 위해서는 전체 페이지의 갯수도 필요하다. 이를 위해서 다음과 같은 쿼리가 필요하다.

    1
    
    SELECT COUNT(id) FROM 테이블;
    
  • MyBatis 사용을 위한 Mapper.xml은 다음과 같이 정리할 수 있다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    <!-- 페이징 처리를 위한 select, limit을 통해 가져오는 데이터 수와 skip data 수를 구한다. -->
    <select id="selectList" resultType="org.zerock.springex.domain.TodoVO">
        SELECT * FROM tbl_todo ORDER BY tno DESC limit #{skip}, #{size}
    </select>
      
    <!-- 전체 tno의 갯수를 가져오기 위함이다. -->
    <select id="getCount" resultType="int">
        SELECT COUNT(tno) FROM tbl_todo
    </select>
    

DTO - RequestDTO

  • 페이지 처리를 위해서는 기본적으로 필요한 정보가 있다. 앞에서 본 페이지 화면을 떠올려보자.

    • 화면에 그리기 위한 “현재 페이지 번호”
    • 한 페이지당 보이는 데이터의 수
  • 이 두 개의 정보는 차후 유지보수의 편의성을 위해 DTO로 만들어서 파라미터로 넘겨주는 것이 좋다.

  • 또한, 페이지 처리를 위해서는 다음 두 가지의 DTO가 필요하다.

    • 페이지 번호와 데이터의 수를 “전달”하기 위한 RequestDTO
    • RequestDTO를 바탕으로 보여주는 페이지 수(end값), previous & next 여부를 표시하기위한 응답 DTO인 ResponseDTO (실제로 화면에 그려지는 정보는 이 값을 이용한다.)
  • 그림으로 나타내면 다음과 같은 관계를 가진다.

    main

  • RequestDTO

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    @Builder
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class PageRequestDTO {
        @Builder.Default
        @Min(value = 1)
        @Positive
        private int page = 1;
          
        @Builder.Default
        @Min(value = 10)
        @Positive
        private int size = 10;
          
        public int getSkip() {
            return (page - 1) * size;
        }
    }
    
    • 생성의 편리함을 위해 lombok의 Buider pattern을 이용하였다.
    • min값과 positive 값을 둬서 “양수”만 페이지 번호가 될 수 있게 하였다.

Paging 처리 로직

  • ResponseDTO를 구성하기 전에, 페이징 처리를 위한 로직들을 이해해보자.

  • 해당 정보들은 전부 화면을 그리기 위해 필요한 정보 들이다. 화면 모양을 기억하면서 해당 정보들이 어디에 어떻게 쓰이는지를 파악하면 이해가 쉬울 것이다:)

  • end (마지막 페이지)

    • 마지막 페이지는 현재 페이지 번호를 size로 나눈 다음, 올림하고 size를 곱해야 한다.
    • 예를 들어,현재 페이지가 11인 경우 size가 10이라면
    • 11/10.0의 올림 => 2이고 2 * size = 20이 끝 번호가 되는 것이다.
    1
    
    this.end = (int)(Math.ceil(this.page/10.0)) * 10;
    
    • 끝 번호를 먼저 구한다면, 시작 번호의 경우 size-1 한 값을 빼주기만 하면 되므로 구하기 쉽다.
  • start (첫번째 페이지)

    1
    
    this.start = this.end - 9; //size-1을 빼준 것이다.
    
  • last (진짜 마지막 페이지)

    • 앞에서 이미 end값을 구했다만, 이는 start를 구하기 위한 임시 값이기에 갱신이 필요하다.
    • 예를 들어, 10개씩 구성하지만 전체 페이지 수가 10으로 나누어 떨어지지 않을 경우, 마지막 페이지에는 10보다 작은 갯수의 페이지가 있을 수 있기 때문이다.
    • 이를 위해 전체 페이지를 기준으로 한 last 값을 구하고, 이것이 end 보다 작을 경우 end값을 last 값으로 갱신해야 한다.
    1
    
    int last = (int)(Math.ceil((total/(double)size)));
    
  • prev (이전 페이지 존재 여부)

    • 시작 페이지가 1이 아니라면 무조건 이전 페이지가 존재한다.
    1
    
    this.prev = this.start > 1;
    
  • next (다음 페이지 존재 여부)

    • 다음 페이지는, 마지막 페이지와 페이지당 개수를 곱한 값이 전체 보다 작다면 다음 페이지가 존재한다.
    1
    
    this.next = total > this.end * this.size;
    

DTO - ResponseDTO

  • 앞에서 사용한 로직들을 이용해서, 화면에 그릴 ResponseDTO 정보를 다음과 같이 구성할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class PageResponseDTO<E> {
    private int page;
    private int size;
    private int total;

    //시작 페이지 번호
    private int start;
    //끝 페이지 번호
    private int end;

    //이전 페이지 존재 여부
    private boolean prev;
    //다음 페이지 존재 여부
    private boolean next;
    //페이지 번호 목록 (dto 리스트)
    private List<E> dtoList;

    @Builder(builderMethodName = "withAll")
    public PageResponseDTO(PageRequestDTO pageRequestDTO, List<E> dtoList, int total) {
        this.page = pageRequestDTO.getPage();
        this.size = pageRequestDTO.getSize();
        this.total = total;
        this.dtoList = dtoList;
        
        this.end = (int) (Math.ceil(page / (double) size)) * 10; // 현재 페이지를 기준으로 끝 페이지 계산
        this.start = end - 9; // 현재 페이지 당 개수는 10개이므로, 9를 빼면 start 페이지를 구할 수 있다.
        int last = (int) (Math.ceil(total / (double) size)) * 10; //전체 페이지에서 나눠서 끝값을 정한다.
        
        this.end = end > last ? last : end; //end가 더 크다면, last가 진짜 끝 값이므로 여기에 맞춘다.
        this.prev = this.start > 1; //1보다 크면 무조건 이전 페이지가 있는 것이다.
        this.next = total > this.end * this.size; //next는 전체 개수가 아직 더 크면 있는 것이다.
    }
}
  • PageResponseDTO 역시 생성의 편리함을 위해 Builder 패턴을 적용하였다.

  • 해당 DTO를 ServiceImpl 클래스에서 사용하기 위해서는 다음과 같이 구성하면 된다.

1
2
3
4
5
6
7
8
9
int total = todoMapper.getCount(pageRequestDTO);
    
PageResponseDTO<TodoDTO> pageResponseDTO = PageResponseDTO.<TodoDTO>withAll()
        .dtoList(dtoList)
        .total(total)
        .pageRequestDTO(pageRequestDTO)
        .build();

return pageResponseDTO;
  • 파라미터에 dtoList가 들어가기 때문에 Mapper에 todoMapper.selectList 를 구성해서 db에서 화면에 띄울 10개의 요소들을 가져오는 과정도 필요하다만, 이번 주제에서는 페이징 로직을 중점으로 보고 있기 때문에 이 부분은 생략하고, PageResponseDTO를 Service에서 어떻게 사용하는지만 간단히 잘라서 보도록 하자.

  • 📌 참고로 중요한 로직이기에 JUnit Test를 작성해서 로직에서 정리한 각 변수들 (start, end…)을 잘 가져오는지 체크하는 것도 잊지 말자.

js & jsp - 페이지 이벤트 처리

  • 구성한 페이지 번호들을 클릭했을 때, 해당 페이지의 list들이 뜰 수 있도록 js를 구성해보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<ul class="pagination justify-content-center">
<!-- 이 부분이 이제 Previous가 나타나는 부분이다. -->
    <c:if test="${responseDTO.prev}">
        <li class="page-item">
            <a class="page-link" data-num="${responseDTO.start - 1}">Previous</a>
        </li>
    </c:if>

    <!-- page 로 넘어갈 수 있도록 forEach 구성 -->
    <c:forEach begin="${responseDTO.start}" end="${responseDTO.end}"  var="num">
        <li class="page-item ${responseDTO.page == num? "active":""}">
            <a class="page-link" data-num="${num}">${num}</a>
        </li>
    </c:forEach>

    <!-- 이게 next를 구성하는 것이다. -->
    <c:if test="${responseDTO.next}">
        <li class="page-item">
            <a class="page-link" data-num="${responseDTO.end + 1}">Next</a>
        </li>
    </c:if>
</ul>
  • 편의를 위해 부트스트랩을 활용하여, 우리 DTO에 맞게 수정하였다.
  • prev, next는 boolean 값이기에 if로 체크해서 표시하도록 하였다.
  • 부트스트랩에서는 active를 통해 style을 적용할 수 있으므로, 해당 num이면 active로 페이지 칸이 색칠 되도록 구성하였다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!-- 페이지 이동을 위한 스크립트 추가, data-num 변수를 이용한다. -->
<script>
    document.querySelectorAll(".pagination").addEventListener('click', function(e) {
        e.preventDefault() // 기본 동작을 막는다.
        e.stopPropagation() // 이벤트 전파를 막는다.

        const target = e.target; //클릭된 실제 DOM 요소를 가져온다.

        //a 태그를 클릭했을때만 동작하도록 하기 위함이다.
        if(target.tagName !== 'A'){
            return;
        }

        // 저장한 data-num 속성값을 가져온다.
        const num = target.getAttribute("data-num");

        self.location = `/todo/list?page=\${num}`; //백틱을 이용해서 템플릿 처리한다.
        //페이지 이동을 자바스크립트로 처리하기 위함이다.
        //여기서 self는 현재 window 객체를 의미한다. (window.location과 동일하다.)
    },false);
</script>
  • js를 이용해서 클릭 이벤트를 처리한다
  • href로 직접 링크를 주는 것보다, 동적 처리나 검색 필터링에 유리한 방식이다.
  • 이를 통해, 해당 a태그의 page-num을 클릭했을때 location을 걸 수 있다.
This post is licensed under CC BY 4.0 by the author.