포스팅 순서는 아래와 같다.
- OpenJDK 17 설치
- STS 4 설치
- 스프링부트 프로젝트 생성
- 메이븐 Local Repository 설정
- DB 설계와 REST API 설계
- mybatis 연동과 게시판 목록 조회
- 페이징 (Mybatis에서 Pageable) (현재 포스팅)
- 동적 정렬처리
Pageable의 오해
Spring Data에서 제공하는 Pageable을 사용하면 페이징 작업을 좀 더 간편하게 개발할 수 있다. 처음에는 Pageable에 대해 알게 되었을 때, JPA에서만 사용할 수 있는 것으로 생각했다. 왜냐하면 대다수의 블로그에서 Spring Data JPA에서 제공되는 기능이라는 식으로 잘못된 정보를 기술하고 있었기 때문이다.
하지만 Mybatis에서도 사용할 수 있다. Spring Data에서는 데이터베이스와 관련된 많은 하위 프로젝트(Spring Data JPA, Spring Data REST, Spring Data JDBC 등)를 포함하는 포괄적인 프로젝트이므로 JPA에만 국한된 것이 아니다.
스프링 프로젝트에서 Pagenation을 쉽게 처리할 수 있는 Pageable 기능을 지금부터 구현해 볼 것이다.
라이브러리 추가
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
</dependency>
RequestList.java 파일 추가
com.company.helloBoard.common.data 패키지에 RequestList.java 파일을 생성한다. 목록 조회 기능 전용의 파일로, 빌더 패턴으로 Pageable를 구현하기 위해 추가하였다.
package com.company.helloBoard.common.data;
import org.springframework.data.domain.Pageable;
import lombok.Builder;
import lombok.Data;
@Builder
@Data
public class RequestList<T> {
private T data;
private Pageable pageable;
}
application.properties 변경
RequestList.java 파일도 패키지명을 생략하고 alias로 사용할 수 있게 하기 위해 추가한다.
# xml 파일의 parameter type, result type에 패키지명 생략 가능하도록 alias 설정
mybatis.type-aliases-package=com.company.helloBoard.*.*.model, com.company.helloBoard.common.data
BoardController.java
package com.company.helloBoard.domain.board.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Pageable;
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 com.company.helloBoard.domain.board.model.Board;
import com.company.helloBoard.domain.board.service.BoardService;
@RestController
@RequestMapping(value = "/boards")
public class BoardController {
@Autowired
BoardService boardService;
@GetMapping("")
public ResponseEntity<?> getListBoard(Board board, @PageableDefault(size = 10) Pageable pageable) {
return ResponseEntity.ok(boardService.getListBoard(board, pageable));
}
}
@PageableDefault 어노테이션을 이용해 페이징 관련 정보를 전달받지 못했을 때 사용할 Pageable 정보를 지정할 수 있다. 페이징은 기본 10개씩 보이도록 디폴트 size를 10으로 설정했다.
BoardService.java
package com.company.helloBoard.domain.board.service;
import java.util.Map;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import com.company.helloBoard.domain.board.model.Board;
public interface BoardService {
public Page<Map<String, Object>> getListBoard(Board board, Pageable pageable);
}
BoardServicImpl.java
package com.company.helloBoard.domain.board.service;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import com.company.helloBoard.common.data.RequestList;
import com.company.helloBoard.domain.board.mapper.BoardMapper;
import com.company.helloBoard.domain.board.model.Board;
@Service
public class BoardServiceImpl implements BoardService {
@Autowired
BoardMapper boardMapper;
@Override
public Page<Map<String, Object>> getListBoard(Board board, Pageable pageable) {
// 빌더 패턴으로 data, pageable 파라미터에 데이터 주입
RequestList<?> requestList = RequestList.builder()
.data(board)
.pageable(pageable)
.build();
List<Map<String, Object>> content = boardMapper.getListBoard(requestList);
int total = boardMapper.getListBoardCount(board);
return new PageImpl<>(content, pageable, total);
}
}
위에서 만든 RequestList.java 파일이 실제로 서비스단에서 쓰이는 부분이다. 빌더 패턴으로 data, pageable 파라미터에 데이터를 주입한 requestList 변수에는 아래와 같이 값이 세팅된다.
PageRequest 객체는 Pageable 인터페이스를 상속받고 있는데, 이 정보에는 정렬 정보, 페이지 offset, page와 같은 정보가 담겨있다. 또한 리턴 타입을 Page<T> 으로 설정했는데 이는 일반적인 게시판 형태의 페이징에서 사용된다.
BoardMapper.java
package com.company.helloBoard.domain.board.mapper;
import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Mapper;
import com.company.helloBoard.common.data.RequestList;
import com.company.helloBoard.domain.board.model.Board;
@Mapper
public interface BoardMapper {
List<Map<String, Object>> getListBoard(RequestList<?> requestList);
int getListBoardCount(Board board);
}
BoardMapper.xml
<?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.company.helloBoard.domain.board.mapper.BoardMapper">
<select id="getListBoard" parameterType="RequestList" resultType="Board">
SELECT BOARD_ID
, WRITER
, TITLE
, CONTENT
, REG_DATE
, UPDATE_DATE
, DELETE_DATE
FROM BOARD
<where>
<if test="data.title != null and data.title != ''">
AND TITLE LIKE '%' || #{data.title} || '%'
</if>
<if test="data.writer != null and data.writer != ''">
AND WRITER LIKE '%' || #{data.writer} || '%'
</if>
</where>
OFFSET #{pageable.offset} ROWS FETCH NEXT #{pageable.pageSize} ROWS ONLY
</select>
<select id="getListBoardCount" parameterType="Board" resultType="int">
SELECT COUNT(*) AS CNT
FROM BOARD
<where>
<if test="title != null and title != ''">
AND TITLE LIKE '%' || #{title} || '%'
</if>
<if test="writer != null and writer != ''">
AND WRITER LIKE '%' || #{writer} || '%'
</if>
</where>
</select>
</mapper>
xml 파일에서 parameterType="RequestList" 으로 변경했다. 서비스단에서 data에는 Board 정보를 담고, pageable에는 Pageable 정보를 담았기 때문에 data.필드, pageable.필드 같은 방법으로 접근이 가능하다. 별다른 설정 없이 Pageable이 제공하는 offset, pageSize를 이용하여 간단하게 페이징을 구현해봤다.
여기까지 구현한 것을 실행해보면 10개의 게시글만 보이는 것을 확인할 수 있다. 아래의 출력 결과를 보면 totalElements, totalPages, size 등 값이 응답값으로 설정되어 있음을 알 수 있다. 이 값들을 통해 뷰에서 페이징을 만들어보려고 한다.
뷰에서 보여주기
뷰에서 페이징 처리를 하기 위해 아래 사진처럼 css, png, js 파일을 추가한다. 이때 png 파일들은 페이지의 <, << 와 같이 화살표 버튼을 이미지화 하기 위해 직접 만들었다.
image 폴더에 페이징 이미지 추가
style.css 파일 추가
.page_prev_on {
background-image: url(../image/page_prev_on.png);
}
.page_prev_off {
background-image: url(../image/page_prev_off.png);
}
.page_left_on {
background-image: url(../image/page_left_on.png);
}
.page_left_off {
background-image: url(../image/page_left_off.png);
}
.page_right_on {
background-image: url(../image/page_right_on.png);
}
.page_right_off {
background-image: url(../image/page_right_off.png);
}
.page_next_on {
background-image: url(../image/page_next_on.png);
}
.page_next_off {
background-image: url(../image/page_next_off.png);
}
.icon {
background-repeat: no-repeat;
background-size: contain;
padding: 15px;
}
.pagination {
display: inline-block;
list-style: none;
}
.pagination li {
color: pink;
float: left;
padding: 8px 16px;
text-decoration: none;
}
com-page.js 파일 추가
const PAGE = {
paging: function(totalPageCount, pageNo, totalElementCount, fn) {
if (totalElementCount == 0) {
document.querySelector("#pagingArea").innerHTML = "";
return false;
}
let pageBlock = 10;
let blockNo = PAGE.toInt(pageNo / pageBlock) + 1;
let startPageNo = (blockNo - 1) * pageBlock;
let endPageNo = blockNo * pageBlock - 1;
if (endPageNo > totalPageCount - 1) {
endPageNo = totalPageCount - 1;
}
let prevBlockPageNo = (blockNo - 1) * pageBlock - 1;
let nextBlockPageNo = blockNo * pageBlock;
let strHTML = "";
// <, << 활성화/비활성화 처리
if (prevBlockPageNo >= 0) {
// <, << 활성화
strHTML += "<li><a href='javascript:" + fn + "(" + 0 + ");' ><span class='icon page_prev_on'></span></a></li>";
strHTML += "<li><a href='javascript:" + fn + "(" + prevBlockPageNo + ");' ><span class='icon page_left_on'></span></a></li>";
} else {
// <, << 비활성화
strHTML += "<li><a><span class='icon page_prev_off'></span></a></li>";
strHTML += "<li><a><span class='icon page_left_off'></span></a></li>";
}
// 페이징 번호 생성
for (let i = startPageNo; i <= endPageNo; i++) {
if (i == pageNo) {
strHTML += "<li class='active'><a>" + (i + 1) + "</a></li>";
} else {
strHTML += "<li><a href='javascript:" + fn + "(" + i + ");' >" + (i + 1) + "</a></li>";
}
}
// >, >> 활성화/비활성화 처리
if (nextBlockPageNo < totalPageCount) {
// >, >> 활성화
strHTML += "<li><a href='javascript:" + fn + "(" + nextBlockPageNo + ");' ><span class='icon page_right_on'></span></a></li>";
strHTML += "<li><a href='javascript:" + fn + "(" + (totalPageCount - 1) + ");' ><span class='icon page_next_on'></span></a></li>";
} else {
// >, >> 비활성화
strHTML += "<li><a><span class='icon page_right_off'></span></a></li>";
strHTML += "<li><a><span class='icon page_next_off'></span></a></li>";
}
let element = document.querySelector("#pagingArea");
element.innerHTML = strHTML;
},
toInt: function(value) {
if (value != null) {
return parseInt(value, 10);
}
},
pageRowNumber: function(pageNo, pageSize, index, totalCount) {
debugger;
if (totalCount) {
return totalCount - ((pageNo) * pageSize + index);
} else {
return (pageNo) * pageSize + (index + 1);
}
}
}
list.html
이전 소스에서 페이징 관련해서 추가된 부분을 ✅ 표시했다.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>게시판</title>
<!-- ✅ 추가 -->
<link type="text/css" href="/css/style.css" rel="stylesheet">
<script type="text/javascript" src="/js/com-ajax.js"></script>
<script type="text/javascript" src="/js/com-page.js"></script>
</head>
<script type="text/javascript">
// 페이지 온로드시 게시글 목록 조회
window.onload = function () {
getList();
}
// 게시글 목록 조회
function getList(pageNo) {
// ✅ 페이지번호 추가
pageNo = pageNo || 0;
const title = document.querySelector("#title").value;
const writer = document.querySelector("#writer").value;
// ✅ 파라미터 전달시에도 추가
const data = "?title=" + title + "&writer=" + writer + "&page=" + pageNo;
AJAX.ajaxCall("GET", "/boards", data, afterGetList);
}
// 조회 후 처리
function afterGetList(response) {
// ✅ 페이징 처리
PAGE.paging(response.totalPages, response.number, response.totalElements, "getList");
// 결과 테이블 생성
resultTable(response);
}
// 동적으로 테이블 생성
function resultTable(response) {
document.querySelector("#fieldListBody").innerHTML = "";
if (response.size > 0) {
const content = response.content;
// ✅ 반복문 변경 (Pageable 결과값을 기준으로 값 가져오기 위함)
for (var i = 0; i < content.length; i++) {
let element = document.querySelector("#fieldListBody");
let result = content[i];
let template = `
<td><p>${PAGE.pageRowNumber(response.number, response.size, i, response.totalElements)}</p></td>
<td><p>${result.title}</p></td>
<td><p>${result.writer}</p></td>
<td><p>${result.regDate}</p></td>
`;
element.insertAdjacentHTML('beforeend', template);
}
}
}
// 초기화
function resetList() {
document.querySelector("#title").value = "";
document.querySelector("#writer").value = "";
document.querySelector("#fieldListBody").innerHTML = "";
getList();
}
</script>
<body>
<div>
<h2>게시판 목록</h2>
<table>
<tr>
<th>제목</th>
<td><input type="text" id="title"></td>
</tr>
<tr>
<th>작성자</th>
<td><input type="text" id="writer"></td>
</tr>
<tr>
<td><button onclick="getList()">조회</button></td>
<td><button onclick="resetList()">초기화</button></td>
</tr>
</table>
<!-- ✅ 페이지 요소 추가 -->
<input type="hidden" name="page" id="page" value="0" />
</div>
<div>
<table>
<colgroup>
<!-- ✅ 페이지 번호 추가 -->
<col width="150px" />
<col width="150px" />
<col width="150px" />
<col width="250px" />
</colgroup>
<thead>
<tr>
<!-- ✅ 페이지 번호 추가 -->
<th>No.</th>
<th>제목</th>
<th>작성자</th>
<th>작성시간</th>
</tr>
</thead>
<tbody id="fieldListBody">
</tbody>
</table>
<!-- ✅ 페이징 표시되는 부분 추가 -->
<ul id = "pagingArea" class="pagination"></ul>
</div>
</body>
</html>
Pageable로 가져온 페이징 정보들을 기반으로 com-page.js 파일에서 pagingArea 변수에 동적으로 페이징 정보를 추가했다. 이때 <, <<, >, >> 같은 페이징 이미지는 style.css에서 가져와서 보여준다. 여기까지 추가한 결과는 아래와 같다. 만약 1, 2, ... 10까지의 블록을 조절하고 싶다면 pageBlock 변수에 값을 변경하면 된다.
다음편 가기 >> 스프링부트 개발환경 구성하기 (8) 동적 정렬처리
'Backend > Spring, SpringBoot' 카테고리의 다른 글
STS 4에서 war 파일로 쉽게 배포하는 방법 (배포 및 실행까지) (0) | 2023.11.29 |
---|---|
스프링부트 개발환경 구성하기 (8) 동적 정렬처리 (1) | 2023.11.13 |
Spring Boot 내장톰캣 war 배포시 한글 깨짐 설정 (0) | 2023.10.19 |
스프링부트 개발환경 구성하기 (6) Mybatis 연동과 게시판 목록 조회 (0) | 2023.10.16 |
스프링부트 개발환경 구성하기 (5) DB 설계와 REST API 설계 (0) | 2023.10.12 |