[JAVA] Pageable로 페이징 처리하기

스프링에서 처음 페이징을 구현할 때 일반적으로 페이징에 필요한 데이터(총 개수, 현재 페이지, 이전/다음 페이지 여부 등)를 만들었는데, 스프링에 만들어놓은 Pageable 인터페이스를 사용해 보았다.
최대한 간단하게 구현하기 위해, 이미 만들어진 코드에서 가공한거라 누락된 부분이 있을 수 있다.
어떻게 사용되는지 힌트만 얻길 바란다.


환경

  • JAVA 17
  • Spring Boot 3.14
  • JPA
  • QueryDSL 5.0.0
  • Thymeleaf

Domain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Entity
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;

    @Column(updatable = false)
    private String userId;

    private String userNm;

    private String userPw;

    private String email; 
}

Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Controller
@RequestMapping("/member")
@RequiredArgsConstructor
public class MemberController {
	private final MemberService memberService;
    
	@GetMapping
	public String list(@Search MemberSearchRequest request, @PageableDefault(size = 10) Pageable pageable, Model model) {
		Page<Member> members = memberService.list(request, pageable);
		model.addAttribute("lists", members);
		model.addAttribute("search", request);

		return "member/list";
	}
}

Dto

1
2
3
4
5
6
@Data
public class MemberSearchRequest {
	private String userId;
	private String userNm;
	private String email;
}

Service

1
2
3
4
5
6
7
8
9
10
@Service
@RequiredArgsConstructor
public class MemberService {
	private final MemberRepository memberRepository;

	@Transactional(readOnly = true)
	public Page<Member> list(MemberSearchRequest request, Pageable pageable) {
		return memberRepository.findByConditions(request, pageable);
	}
}

Repository

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@RequiredArgsConstructor
public class MemberRepository {
    private final JPAQueryFactory queryFactory;
    private final QMember member = QMember.member;

    public Page<Member> findByConditions(MemberSearchRequest request, Pageable pageable) {
        List<Member> members = queryFactory
                .selectFrom(member)
                .where(isConditions(request))
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .orderBy(member.id.asc())
                .fetch();

        JPAQuery<Long> countQuery = queryFactory
                .select(member.count())
                .from(member)
                .where(isConditions(request));

        return PageableExecutionUtils.getPage(members, pageable, countQuery::fetchOne);
    }

    private BooleanBuilder isConditions(MemberSearchRequest request) {
        BooleanBuilder builder = new BooleanBuilder();

        return builder
                .and(containsUserId(request.getUserId()))
                .and(containsEmail(request.getEmail()))
                .and(containsUserNm(request.getUserNm()));
    }

    private BooleanExpression containsUserId(String userId) {
        return StringUtils.hasText(userId) ? member.userId.contains(userId) : null;
    }

    private BooleanExpression containsEmail(String email) {
        return StringUtils.hasText(email) ? member.email.contains(email) : null;
    }

    private BooleanExpression containsUserNm(String UserNm) {
        return StringUtils.hasText(UserNm) ? member.userNm.contains(UserNm) : null;
    }
}

Thymeleaf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<div class="container-fluid pt-3 pb-3">
	<form class="w-25 mb-4" th:action="@{/member}" th:object="${search}">
		<div class="row mb-3">
			<label th:for="userId" class="col-sm-2 col-form-label">아이디</label>
			<div class="col-sm-8">
				<input type="text" class="form-control" th:field="*{userId}">
			</div>
		</div>
		<div class="row mb-3">
			<label th:for="userNm" class="col-sm-2 col-form-label">이름</label>
			<div class="col-sm-8">
				<input type="text" class="form-control" th:field="*{userNm}">
			</div>
		</div>
		<div class="row mb-3">
			<label th:for="email" class="col-sm-2 col-form-label">이메일</label>
			<div class="col-sm-8">
				<input type="text" class="form-control" th:field="*{email}">
			</div>
		</div>
		<div class="row">
			<div class="col-sm-10">
				<button class="form-control btn btn-dark" type="submit">조회</button>
			</div>
		</div>
	</form>

	<table class="table table-striped table-hover">
		<tr>
			<th class="text-center">아이디</th>
			<th class="text-center">이름</th>
			<th class="text-center">이메일</th>
		</tr>
		<th:block th:if="${!#lists.isEmpty(memberList)}" th:each="member : ${memberList}">
			<tr th:data-member-id="${member.id}">
				<td class="align-middle">
					<div th:text="${member.userId}"></div>
				</td>
				<td>
					<input class="form-control" type="text" name="userNm" th:value="${member.userNm}">
				</td>
				<td>
					<input class="form-control" type="text" name="email" th:value="${member.email}">
				</td>
			</tr>
		</th:block>
	</table>

	<nav aria-label="Page navigation">
		<ul class="pagination justify-content-center">
			<li class="page-item" th:classappend="${!memberList.hasPrevious} ? 'disabled' : ''">
				<a class="page-link" th:href="@{/member(page=${memberList.number - 1}, userId=${search.userId}, userNm=${search.userNm}, email=${search.email})}" aria-label="Previous">
					<span aria-hidden="true">&laquo;</span>
				</a>
			</li>

			<li th:each="page : ${#numbers.sequence(0, memberList.totalPages - 1)}" class="page-item" th:classappend="${page == memberList.number} ? 'active' : ''">
				<a class="page-link" th:href="@{/member(page=${page}, userId=${search.userId}, userNm=${search.userNm}, email=${search.email})}" th:text="${page + 1}"></a>
			</li>

			<li class="page-item" th:classappend="${!memberList.hasNext} ? 'disabled' : ''">
				<a class="page-link" th:href="@{/member(page=${memberList.number + 1}, userId=${search.userId}, userNm=${search.userNm}, email=${search.email})}" aria-label="Next">
					<span aria-hidden="true">&raquo;</span>
				</a>
			</li>
		</ul>
	</nav>

</div>

Categories:

Updated:

Leave a comment