반응형

페이징(Paging, Pagination) 이란?

  • 게시판에 100개의 글이 있는 상황
  • 게시판의 글 리스트를 조회할 때, 한 번에 100개의 글을 불러오기에는 데이터의 양이 너무 많음
  • 이런 상황에서는 페이징 처리가 필요함
    • ex) 한 페이지에 글 10개씩 출력하도록 설정할 수 있음 => 1 ~ 10번 글은 1페이지에 출력, 11 ~ 20번 글은 2페이지에서 출력, ...
  • 페이징 기능에는 정렬 기능이 포함되어 있음
    • ex) 글의 id순, 최신순, 이름순 등으로 정렬하여 출력할 수 있음 => 가장 최신 10개의 글은 1페이지에 출력, 다음 최신 10개의 글은 2페이지에 출력, ...

페이징 방법

  • Spring Boot에서 Paging 기능을 구현할 수 있는 2가지 방법 존재
    • Pageable 사용
    • PageRequest 사용 (PageRequest는 Pageable의 구현체임)

페이징 예제

Repository

public interface UserRepository extends JpaRepository<User, Long> {
    Page<User> findAll(Pageable pageable);
    Page<User> findByNameContains(String name, Pageable pageable);
}
  • 조건에 만족하는 모든 데이터들을 불러오기 위해서는 return 타입을 List로 해주었음
  • 페이징 기능을 적용하고 싶다면 return 타입을 Page와 같이 지정해주고, parameter로 Pageable을 넣어주면 됨

Controller

// Pageable 사용 예제
@GetMapping("/all-users")
public Page<User> getAllUser(@PageableDefault(size = 5, sort = "age", direction = Sort.Direction.ASC)) {
    return userRepository.findAll(pageable);
}

// PageRequest 사용 예제
@GetMapping("/find-by-name")
public Page<User> findByName(@RequestParam(required = false, defaultValue = "0") int page) {
    PageRequest pageable = PageRequest.of(page, 10, Sort.by("name").descending());
    return userRepository.findByNameContains("kim", pageable);
}
  • getAllUser에서는 @PageableDefault를 사용하여 페이징 처리 (나이를 기준으로 오름차순 정렬)
    • /all-users?page=2 와 같이 뒤에 Query Parameter을 통해 page 이동 가능
  • findByName에서는 PageRequest를 직접 생성하여 페이징 처리 (이름을 기준으로 내림차순 정렬)
    • @PageableDefault와 달리 직접 Query Parameter을 받는 작업을 해줘야 함
    • PageRequest.of의 파라미터는 순서대로 (페이지 번호, 사이즈, 정렬 방식)을 뜻함

페이지 정보 추출

  • Page/에는 User들에 대한 정보 뿐만 아니라 페이지 정보도 담겨져 있음
  • 아래와 같은 정보들이 담겨 있음
    • 화면 생성 시 아래 정보들을 사용해 페이징 처리 가능
List<User> users = result.getContent();     // User들에 대한 정보
int nowPageNumber = result.getNumber();     // 현재 페이지 번호
int totalPages = result.getTotalPages();    // 전체 페이지 수
int pageSize = result.getSize();        // 한 페이지에 출력되는 데이터 개수
boolean hasNextPage = result.hasNext();     // 다음 페이지 존재 여부
boolean hasPreviousPage = result.hasPrevious(); // 이전 페이지 존재 여부
boolean isFirstPage = result.isFirst();     // 첫번째 페이지 인지
boolean isLastPage = result.isLast();       // 마지막 페이지 인지

1 페이지 부터 시작 방법

  • 위와 같이 별도의 설정이 없다면 0 페이지 부터 시작함
  • 페이징 작업을 1 페이지 부터 시작하는 방법은 아래의 Bean을 등록해주면 됨
@Bean
public PageableHandlerMethodArgumentResolverCustomizer customize() {
    return p -> {
        p.setOneIndexedParameters(true);    // 1 페이지 부터 시작
        p.setMaxPageSize(10);       // 한 페이지에 10개씩 출력
    };
}
  • 위 방법을 사용하면 Pageable의 default가 1 페이지 부터 시작하게 됨
  • 이 때, 주의할 점은 Pageable의 설정이 바뀐 것이기 때문에 PageRequest.of를 사용하였다면 아래와 같이 수정이 필요함
// PageRequest 사용 예제
@GetMapping("/find-by-name")
public Page<User> findByName(@RequestParam(required = false, defaultValue = "1") int page) {
    PageRequest pageable = PageRequest.of(page - 1, 10, Sort.by("name").descending());
    return userRepository.findByNameContains("kim", pageable);
}

페이징, 정렬, 검색, 알림창 띄우기를 활용한 예제

  • Gamer라는 객체에 name, age, rank 정보를 넣음
  • 임의의 Gamer 25명 추가
  • 한 페이지에 7명씩 출력
  • 나이와 랭크를 기준으로 검색 가능
  • 아이디, 나이와 랭크를 기준으로 오름차순, 내림차순 정렬 가능
  • 검색 조건이 정상적이지 않다면(ex) 19살 이상 18살 이하) 에러 메세지 출력

Gamer

@Entity
@Data
public class Gamer {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private int age;

    @Column(name = "\"rank\"")
    private Rank rank;
}

Rank

public enum Rank{
    BRONZE, SILVER, GOLD, PLATINUM, DIAMOND
}

SortType

@Getter
@AllArgsConstructor
public enum SortType {

    ID_ASC("아이디 오름차순"),
    ID_DESC("아이디 내림차순"),
    AGE_ASC("나이 오름차순"),
    AGE_DESC("나이 내림차순"),
    RANK_ASC("랭크 오름차순"),
    RANK_DESC("랭크 내림차순");

    private final String description;
}

SearchForm

  • Gamer의 나이가 ageGe이상 ageLe이하인 Gamer을 검색할 수 있음
  • Gamer의 랭크가 rankGe이상 rankLe이하인 Gamer을 검색할 수 있음
  • 정렬 타입 선택 가능
@Data
public class SearchForm {
    private Integer ageGe;
    private Integer ageLe;

    private Rank rankGe;
    private Rank rankLe;

    private SortType sortType;
}

GamerRepository

public interface GamerRepository extends JpaRepository<Gamer, Long> {
    Page<Gamer> findByAgeGreaterThanEqualAndAgeLessThanEqualAndRankGreaterThanEqualAndRankLessThanEqual(
            Integer ageGe, Integer ageLe, Rank rankGe, Rank rankLe, Pageable pageable);
}

GamerController

@Controller
@RequestMapping("/pagination-sort-example")
@RequiredArgsConstructor
public class GamerController {

    private final GamerRepository gamerRepository;

    @GetMapping("/gamers")
    public String getGamers(@RequestParam(required = false, defaultValue = "1") int page,
                            Model model, @ModelAttribute SearchForm form) {

        // 검색 조건
        if (form.getAgeGe() == null) form.setAgeGe(0);
        if (form.getAgeLe() == null) form.setAgeLe(999);
        if (form.getRankGe() == null) form.setRankGe(Rank.BRONZE);
        if (form.getRankLe() == null) form.setRankLe(Rank.DIAMOND);

        if (form.getAgeGe() > form.getAgeLe() || form.getRankGe().compareTo(form.getRankLe()) == 1) {
            model.addAttribute("message", "검색 조건 에러");
            model.addAttribute("nextUrl", "/pagination-sort-example/gamers");
            return "pagination_sort_example/message";
        }

        // 정렬 조건
        if (form.getSortType() == null) form.setSortType(SortType.ID_ASC);
        SortType sortType = form.getSortType();

        PageRequest pageRequest = PageRequest.of(page - 1, 10);
        if (sortType == SortType.ID_ASC) pageRequest = PageRequest.of(page - 1, 7, Sort.by("id").ascending());
        else if (sortType == SortType.ID_DESC) pageRequest = PageRequest.of(page - 1, 7, Sort.by("id").descending());
        else if (sortType == SortType.AGE_ASC) pageRequest = PageRequest.of(page - 1, 7, Sort.by("age").ascending());
        else if (sortType == SortType.AGE_DESC) pageRequest = PageRequest.of(page - 1, 7, Sort.by("age").descending());
        else if (sortType == SortType.RANK_ASC) pageRequest = PageRequest.of(page - 1, 7, Sort.by("rank").ascending());
        else if (sortType == SortType.RANK_DESC) pageRequest = PageRequest.of(page - 1, 7, Sort.by("rank").descending());
        else {
            model.addAttribute("message", "정렬 조건 에러");
            model.addAttribute("nextUrl", "/pagination-sort-example/gamers");
            return "pagination_sort_example/message";
        }

        Page<Gamer> gamers =
                gamerRepository.findByAgeGreaterThanEqualAndAgeLessThanEqualAndRankGreaterThanEqualAndRankLessThanEqual(
                        form.getAgeGe(), form.getAgeLe(), form.getRankGe(), form.getRankLe(), pageRequest);

        model.addAttribute("gamers", gamers);
        model.addAttribute("searchForm", form);

        model.addAttribute("ranks", Rank.values());
        model.addAttribute("sortTypes", SortType.values());
        return "pagination_sort_example/home";
    }
}

home.html

  • searchForm을 통해 검색, 정렬 조건을 입력 받음
  • method를 get으로 해줌으로써 URL에 Query Parameter로 전송
  • 페이징 처리할 때, page 정보 뿐만 아니라 검색 조건과 정렬 조건을 같이 넘겨줘야 검색 결과를 유지한채 페이지 이동 가능
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Pagination Example</title>
</head>
<body>
<div style="width: 500px;">
    <form th:object="${searchForm}" th:method="get" action="/pagination-sort-example/gamers">
        <div>
            <input type="number" th:field="*{ageGe}" th:value="*{ageGe}" style="width: 50px">
            <label th:for="ageGe">살 이상</label>
            <span>&nbsp;&nbsp;&nbsp;&nbsp;</span>
            <input type="number" th:field="*{ageLe}" th:value="*{ageLe}" style="width: 50px">
            <label th:for="ageLe">살 이하</label>
        </div>
        <br/>
        <div>
            <select th:field="*{rankGe}">
                <option th:each="rank: ${ranks}" th:value="${rank}" th:text="${rank}" />
            </select>
            <label th:for="*{rankGe}">이상</label>
            <span>&nbsp;&nbsp;&nbsp;&nbsp;</span>
            <select th:field="*{rankLe}">
                <option th:each="rank: ${ranks}" th:value="${rank}" th:text="${rank}" />
            </select>
            <label th:for="*{rankLe}">이상</label>
        </div>
        <br/>
        <div>
            <span>정렬 기준 : </span>
            <select th:field="*{sortType}">
                <option th:each="sortType: ${sortTypes}" th:value="${sortType}" th:text="${sortType.description}" />
            </select>
        </div>
        <br/>
        <div>
            <button type="submit">검색</button>
        </div>
    </form>
    <br/><hr/>
</div>
<div>
    <h3>검색 나이 범위 : [[${searchForm.ageGe}]] ~ [[${searchForm.ageLe}]]</h3>
    <h3>검색 랭크 범위 : [[${searchForm.rankGe}]] ~ [[${searchForm.rankLe}]]</h3>
    <h3>정렬 기준 : [[${searchForm.sortType.description}]]</h3>
    <h3>검색 개수 : [[${gamers.getTotalElements()}]]</h3>
    <h3>현재 페이지 : [[${gamers.getNumber() + 1}]]</h3>
</div>
<div style="width: 500px; text-align: center;" align="center">
    <hr/><br/>
    <table align="center">
        <tr align="center">
            <th style="width: 50px;">#</th>
            <th style="width:100px;">이름</th>
            <th style="width: 50px;">나이</th>
            <th style="width:130px;">랭크</th>
        </tr>
        <tr th:each="gamer : ${gamers}" align="center">
            <td th:text="${gamer.id}"/>
            <td th:text="${gamer.name}"/>
            <td th:text="${gamer.age}"/>
            <td th:text="${gamer.rank}"/>
        </tr>
    </table>
    <br/><br/>
    <button th:disabled="${!gamers.hasPrevious()}"
            th:onclick="|location.href='@{/pagination-sort-example/gamers(page=${gamers.getNumber()}, ageGe=${searchForm.getAgeGe()}, ageLe=${searchForm.getAgeLe()}, rankGe=${searchForm.getRankGe()}, rankLe=${searchForm.getRankLe()}, sortType=${searchForm.getSortType()})}'|">
        이전 페이지</button>
    <span>[[${gamers.getNumber() + 1}]] / [[${gamers.getTotalPages()}]] page</span>
    <button th:disabled="${!gamers.hasNext()}"
            th:onclick="|location.href='@{/pagination-sort-example/gamers(page=${gamers.getNumber() + 2}, ageGe=${searchForm.getAgeGe()}, ageLe=${searchForm.getAgeLe()}, rankGe=${searchForm.getRankGe()}, rankLe=${searchForm.getRankLe()}, sortType=${searchForm.getSortType()})}'|">
        다음 페이지</button>
    <br/><br/><hr/><br/>
</div>
<div>

</div>
</body>
</html>

message.html

  • JavaScript를 활용해 넘어온 메세지 출력 후 다음 URL로 이동
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Pagination Example</title>
</head>
<body>
<script th:inline="javascript">
    window.onload = function () {
        alert([[${message}]])
        window.location.href = [[${nextUrl}]]
    }
</script>
</body>
</html>

결과

  • 메인 페이지

  • 페이지 이동

  • 18살25살, SILVERPLATINUM에 해당하는 Gamer을 랭크 기준으로 오름차순한 결과

  • 검색 후 페이지 이동

  • 31살 이상 22살 이하 검색 시 => 에러 메세지 출력

 

반응형

↓ 클릭시 이동

복사했습니다!