spring mvc / jsp 페이징 처리
여기서는 limit 기능이 존재하는 DBMS를 대상으로 페이징 처리를 다룹니다.
Spring MVC 패턴으로 lombok과 mybatis를 이용한다고 가정합니다.
LIMIT
SELECT * FROM 테이블이름 ORDER BY tno DESC LIMIT 0, 10; -- 0개 건너 뛰고 10개 표시: 즉 1 page 보기
SELECT * FROM 테이블이름 ORDER BY tno DESC LIMIT 10, 10; -- 10개 건너 뛰고 10개 표시:즉 2 page 보기
SELECT * FROM 테이블이름 ORDER BY tno DESC LIMIT 20, 10; -- 20개 건너 뛰고 10개 표시:즉 3 page 보기
위 예제를 보면 아시겠지만 limit 건너뛰어야할게시물수, 가져와야할게시물수.
이렇게 해당 페이지를 가져오도록 되어있습니다.
건너뛰어야할 게시물 수에는 수식을 넣을 수 없고 숫자만 넣어야 해서 프로그래밍 언어에서 숫자를 잘 조작해서 SQL을 실행할 때 숫자로 넣어줘야 합니다.
PageRequestDTO
페이징 처리용 DTO 객체를 만들어 둡니다.
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageRequestDTO {
@Builder.Default
private int page = 1; //페이지 번호
@Builder.Default
private int size = 10; //한 페이당 보여주는 게시물 개수
private String link;
//건너뛰는 게시물 수
public int getSkip(){
return (page - 1) * 10;
}
public String getLink(){
if(link == null){
StringBuffer sb = new StringBuffer();
sb.append("page=").append(this.page);
sb.append("&size=").append(this.size);
link = sb.toString();
}
return link;
}
}
Mapper interface
Mybatis와 연결할 매퍼 인터페이스를 만듭니다.
public interface SampleMapper {
//SQL들은 .xml 파일에 정의하고 mapper 인터페이스를 xml 파일의 네임스페이스로 연결 필요.
List<sampleVO> selectList(PageRequestDTO pageRequestDTO); //DB에서 가져온 데이터는 sampleVO에 담는다고 가정
int getCount(PageRequestDTO pageRequestDTO); //게시물 총 개수
}
PageMapper.xml
사용할 SQL문들을 xml 파일에 등록합니다.
<select id="selectList" resultType="net.aacii.spring.domain.TodoVO">
SELECT * FROM 테이블이름 ORDER BY tno DESC LIMIT #{skip}, #{size}
</select>
<select id="getCount" resultType="int">
SELECT count(tno) FROM 테이블이름
</select>
PageResponseDTO
sampleDTO 의 목록, 전체 데이터의 수, 페이지 처리를 위한 데이터들(시작 페이지 번호, 끝 페이지 번호)로 구성된 DTO를 만듭니다.
현재 페이지 번호(page)와 페이지당 데이터의 수(size)를 이용해서 화면상의 페이지 번호를 구해야 합니다.
그래서 PageResponseDTO는 생성자를 통해 필요한 page나 size 등을 전달 받아야 합니다.
@Getter
@ToString
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; //다음 페이지의 존재 여부
private List<E> dtoList; //데이터 리스트
//생성자
@Builder(builderMethodName = "withAll")
public PageResponseDTO(PageRequestDTO pageRequestDTO, int total, List<E> dtoList){
this.page = pageRequestDTO.getPage();
this.size = pageRequestDTO.getSize();
this.total = total;
this.dtoList = dtoList;
this.end = (int)(Math.ceil(this.page / 10.0)) * 10;
this.start = this.end - 9;
int last = (int)(Math.ceil((total / (double)size)));
this.end = Math.min(end, last);
this.prev = this.page > 1;
this.next = total > this.end * this.size;
}
}
마지막 페이지(end)를 먼저 구해야 시작 페이지(start)를 구하기 쉬워집니다.
마지막 페이지 계산
this.end = (int)(Math.ceil(this.page / 10.0)) * 10;
시작 페이지 계산
this.start = this.end -9;
마지막 페이지의 경우 전체 개수(total)를 고려해야합니다.
만약 10개씩 보여주는 경우 전체 개수가 72라면 마지막 페이지는 8 이어야 하기 때문입니다.
int last = (int)(Math.ceil((total/(doble)size)));
마지막 페이지(end)는 last 값 보다 작은경우 last 값이 end가 되어야 합니다.
this.end = end > last ? last : end;
이전 페이지와 다음페이지 계산
1페이지를 제외하고 무조건 이전 페이지는 true 이어야 합니다.
마지막 페이지(end)와 페이지당 개수(size)를 곱한 값보다 전체 개수(total)가 많은지 검사해봐야 합니다.
this.prev = this.start > 1;
this.next = total > this.end * this.size;
Service 인터페이스
PageRequestDTO를 매개변수로 받아서 DB에서 가져온 데이터를 SampleDTO(게시물DTO)로 되어진 리스트를 만들어서 PageResponseDTO 데이터 타입으로 리턴해주는 인터페이스를 작성합니다.
public interface SampleService {
PageResponseDTO<SampleDTO> getList(PageRequestDTO pageRequestDTO);
}
Service 인터페이스 구현체
SampleDTO와 SampleVO는 테이블 구조에 따라 달라지니 생략합니다.
@Service
@Log4j2
@RequiredArgsConstructor
public class SampleServiceImpl implements SampleService{
private final SampleMapper sampleMapper;
private final ModelMapper modelMapper; //모델매퍼 설정은 따로 언급
@Override
public PageResponseDTO<SampleDTO> getList(PageRequestDTO pageRequestDTO) {
//request로 받은 DTO를 이용해서 DB에서 가져온 VO들을 list에 담음
List<SampleVO> voList = todoMapper.selectList(pageRequestDTO);
//VO 리스트의 스트림을 얻어서 모델 매퍼로 DTO로 변환한 뒤 이 DTO들을 다시 리스트에 담음
List<SampleDTO> dtoList = voList.stream()
.map(vo->modelMapper.map(vo, SampleDTO.class))
.collect(Collectors.toList());
//총 개시물 개수 구하기
int total = todoMapper.getCount(pageRequestDTO);
//페이징된 결과를 response에 맞는 데이터로 가공하기 위해서
//PageResponseDTO의 생성자를 통해 데이터를 설정해야하는데,
//그 과정에서 필요한 데이터들은 위에서 구한 값로 세팅해서 빌드한뒤 리턴
PageResponseDTO<TodoDTO> pageResponseDTO = PageResponseDTO.<SampleDTO>withAll()
.dtoList(dtoList)
.total(total)
.pageRequestDTO(pageRequestDTO)
.build();
return pageResponseDTO;
}
모델 매퍼 설정
import org.modelmapper.ModelMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ModelMapperConfig {
@Bean
public ModelMapper getMapper() {
ModelMapper mapper = new ModelMapper();
mapper.getConfiguration()
.setFieldMatchingEnabled(true)
.setFieldAccessLevel(org.modelmapper.config.Configuration.AccessLevel.PRIVATE)
.setMatchingStrategy(org.modelmapper.convention.MatchingStrategies.STRICT);
return mapper;
}
}
root-context.xml에도 위 모델 매퍼 설정을 bean 객체로 스캔하기 위해 패키지 경로를 등록 해줘야 합니다.
<!-- 모델 매퍼 설정 스프링 bean 스캔 경로 지정 -->
<context:component-scan base-package="ModelMapperConfig클래스의패키지경로"/>
JSP 화면
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!-- 생략... BootStrap 5를 사용한다고 가정-->
<div class="float-end">
<ul class="pagination flex-wrap">
<c:if test="${responseDTO.prev}">
<li class="page-item">
<a class="page-link" data-num="${responseDTO.start -1}">Prev</a>
</li>
</c:if>
<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>
<c:if test="${responseDTO.next}">
<li class="page-item">
<a class="page-link" data-num="${responseDTO.end + 1}">Next</a>
</li>
</c:if>
</ul>
<script>
document.querySelector(".pagination").addEventListener("click", function(e){
e.preventDefault();
e.stopPropagation();
const target = e.target
if(target.tagName !== 'A'){
return;
}
const num = target.getAttribute("data-num")
self.location = `/todo/list?page=\${num}` //backtic(`): 작은 따옴표 아님 주의
}, false);
</script>
</div>