DEV&OPS/Java

spring mvc / jsp 페이징 처리

ALEPH.GEM 2025. 11. 7. 09:19

여기서는 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>

 

 

 

 

 

 

 

 

 

 

 

 

728x90