이번 포스트에서는 user 페이지에 남아 있던 검색 기능, 페이지네이션, 코드 리팩토링을 마무리하고, 최종적으로 불필요한 form 페이지와 관련 코드를 정리합니다.
최종 완성된 결과는 아래와 같이 나타납니다.
최종결과 이미지

form.html 페이지와 관련된 컨트롤러, 서비스 코드는 모두 삭제합니다.
해당 페이지는 form 태그를 다루는 학습용 예제였기 때문에, 이제 실사용에 포함되지 않습니다.
먼저 Bootstrap에서 기본 제공하는 pagination 코드를 활용해 UI를 구성합니다.
<nav aria-label="Page navigation example">
<ul class="pagination">
<li class="page-item"><a class="page-link" href="#">Previous</a></li>
<li class="page-item"><a class="page-link" href="#">1</a></li>
<li class="page-item"><a class="page-link" href="#">2</a></li>
<li class="page-item"><a class="page-link" href="#">3</a></li>
<li class="page-item"><a class="page-link" href="#">Next</a></li>
</ul>
</nav>
페이지네이션 처리를 위해 @PageableDefault 어노테이션을 사용해 페이징 정보를 컨트롤러에서 바로 받을 수 있습니다.
@GetMapping("/users")
public String userView(Model model, @PageableDefault(size = 20) Pageable pageable) {
UserPageDto userPageDto = userService.getAllUsers(pageable);
model.addAttribute("startPage", userPageDto.getStartPage());
model.addAttribute("endPage", userPageDto.getEndPage());
model.addAttribute("users", userPageDto.getUsers());
return "/user/user";
}
📌 참고
URL: http://localhost:8080/admin/users?page=1&size=1JPA에서는 Pageable을 findAll()에 그대로 넘겨주기만 하면 됩니다.
이를 통해 자동으로 LIMIT, OFFSET 처리가 이루어집니다.
// UserService
public UserPageDto getAllUsers(Pageable pageable) {
Page<User> users = userRepository.findAll(pageable);
return UserPageDto.of(users);
}
페이지 정보와 함께 사용자 데이터를 담는 DTO입니다.
기본 구조는 다음과 같습니다.
//UserPageDto
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class UserPageDto {
private Page<User> users;
private int startPage;
private int endPage;
public static UserPageDto of(Page<User> users) {
int currentPage = users.getNumber();
int totalPages = users.getTotalPages();
int startPage = Math.max(1, currentPage - 4);
int endPage = Math.min(totalPages, currentPage + 4);
return new UserPageDto(users, startPage, endPage);
}
}
📝 여기선 startPage, endPage 등 페이지 범위 정보도 포함할 수 있도록 확장 가능합니다.
HTML에서 Page 객체를 다루기 위해선 다음처럼 접근하면 됩니다:
🔁 HTML 페이지네이션 반복 코드 (Thymeleaf)
@GetMapping("/users")
public String userView(Model model, @PageableDefault(size = 20) Pageable pageable ) {
UserPageDto userPageDto = userService.getAllUsers(pageable);
model.addAttribute("users", userPageDto.getUsers());
return "/user/user";
}
user.html
<div class="text-end">
총 건수 : <span th:text="${users.totalElements}">0</span>
</div>
...
<nav aria-label="Page navigation example">
<ul class="pagination justify-content-center">
<li class="page-item">
<a
class="page-link"
href="#"
th:classappend="${1 == users.pageable.pageNumber + 1} ? 'disabled'"
th:href="@{/admin/users(page=${users.pageable.pageNumber - 1}, searchText = ${param.searchText}, sort=${sort})}"
><</a
>
</li>
<li
class="page-item"
th:classappend="${i == users.pageable.pageNumber + 1} ? 'disabled'"
th:each="i : ${#numbers.sequence(startPage, endPage)}"
>
<a
class="page-link"
href="#"
th:text="${i}"
th:href="@{/admin/users(page=${i - 1}, searchText = ${param.searchText}, sort=${sort})}"
>1</a
>
</li>
<li
class="page-item"
th:classappend="${users.totalPages == users.pageable.pageNumber + 1} ? 'disabled'"
>
<a
class="page-link"
href="#"
th:href="@{/admin/users(page=${users.pageable.pageNumber + 1}, searchText = ${param.searchText}, sort=${sort})}"
>></a
>
</li>
</ul>
</nav>
이와 같은 코드로 페이지네이션 구현 끝
이번에는 user 페이지에 검색 기능과 정렬 기능을 연동해보겠습니다.
검색어를 입력하고 정렬 기준을 바꿔도 페이지 이동 시 해당 정보가 유지되도록 구현할 것입니다.
GET 방식의 form을 사용해 검색어와 정렬 기준을 유지한 채로 페이지를 이동할 수 있게 구성합니다.
<form
class="row g-3 d-flex justify-content-center"
method="GET"
th:action="@{/admin/users}"
>
<div class="col-4">
<label for="searchText" class="visually-hidden">검색</label>
<input
type="text"
class="form-control"
id="searchText"
name="searchText"
th:value="${param.searchText}"
/>
</div>
<div class="col-auto">
<select class="form-select" name="sort" onchange="this.form.submit()">
<option th:value="idAsc" th:selected="${sort} == 'idAsc'">ID 오름차순</option>
<option th:value="idDesc" th:selected="${sort} == 'idDesc'">ID 내림차순</option>
<option th:value="nameAsc" th:selected="${sort} == 'nameAsc'">이름 오름차순</option>
</select>
</div>
<div class="col-auto">
<button type="submit" class="btn btn-dark mb-3">검색</button>
</div>
<div class="col-auto">
<a class="btn btn-secondary mb-3" href="/admin/users">초기화</a>
</div>
</form>
검색어(searchText)와 정렬 기준(sort)을 @RequestParam으로 받아 처리합니다.
@GetMapping("/users")
public String userView(
Model model,
@PageableDefault(size = 20) Pageable pageable,
@RequestParam(required = false, defaultValue = "")
String searchText,
@RequestParam(defaultValue = "idAsc") String sort) {
UserPageDto userPageDto = userService.getAllUsers(pageable, searchText, sort);
model.addAttribute("startPage", userPageDto.getStartPage());
model.addAttribute("endPage", userPageDto.getEndPage());
model.addAttribute("users", userPageDto.getUsers());
model.addAttribute("sort", sort);
return "/user/user";
}
public UserPageDto getAllUsers(Pageable pageable, String searchText, String sort) {
Pageable sortedPageable = PageRequest.of(
pageable.getPageNumber(),
pageable.getPageSize(),
getSortOption(sort)
);
Page<User> users = userRepository.findAllByNameContainingOrEmailContainingOrMobileNumberContainingOrAddressContaining(
searchText, searchText, searchText, searchText, sortedPageable
);
return UserPageDto.of(users);
}
정렬 기준은 Map으로 깔끔하게 관리합니다.
private Sort getSortOption(String sort) {
Map<String, Sort> sortMap = Map.of(
"idAsc", Sort.by(Sort.Direction.ASC, "id"),
"nameAsc", Sort.by(Sort.Direction.ASC, "name"),
"idDesc", Sort.by(Sort.Direction.DESC, "id") // 기본값
);
return sortMap.getOrDefault(sort, Sort.by(Sort.Direction.DESC, "id"));
}
Spring Data JPA의 메서드 이름 쿼리 방식입니다. 아래 조건 중 하나라도 해당하면 데이터를 조회합니다:
`SELECT *
FROM user
WHERE name LIKE '%searchText%'
OR email LIKE '%searchText%'
OR mobile_number LIKE '%searchText%'
OR address LIKE '%searchText%'`
이제 유저 페이지에서 다음 기능이 완벽히 동작합니다: