πνκ²½
IntelliJ Ultimate
Java 17
SpringBoot 3.2.3
Gradle - Groovy
Dependencies:
Spring Web
Thymeleaf
Spring Data JPA
lombok
MariaDB 10.11
Spring Dev tool
QueryDSL 5.0.0
πνμ΄μ§λ€μ΄μ (Pagination)μ΄λ?
νμ΄μ§ λλ νμ΄μ§λ€μ΄μ μ΄λΌκ³ λΆλ₯Έλ€. λ€λμ λ°μ΄ν°λ₯Ό 곡ν΅λ κ°μλ§νΌ μ¬λ¬ λ¬ΆμμΌλ‘ λλμ΄ νΉμ νμ΄μ§ λΉ ν λ¬Άμμ νλ©΄μ 보μ¬μ£Όλ κ²μ λ§νλ€.
DBμ λ±λ‘λ λ°μ΄ν°κ° λ§μ κ²½μ° μμ² μ μ 체λ₯Ό νλ²μ λ€κ³ μ¨λ€λ©΄ μλ΅ μκ°μ΄ κΈΈμ΄μ§κ² λλ©΄μ ν΄λΌμ΄μΈνΈμκ² λΆνΈμ μ΄λνλ€. μ΄λ λ°μ΄ν°λ₯Ό νμ΄μ§μ ν΄λΉνλ κ°μλ§νΌ μΌλΆλ§ μλΌμ κ°μ Έμ¨λ€λ©΄ μκ°μ λ¨μΆν μ μλ€. μλ₯Ό λ€μ΄ νλ©΄μμ 1νμ΄μ§λ₯Ό μμ²νλ€λ©΄ λ°±μλμμλ μ λ ¬ν λ°μ΄ν° μ€ μ²«λ²μ§Έ λ°μ΄ν°λΆν° 10λ²μ§Έ λ°μ΄ν°κΉμ§λ§ μλΌμ λ°ννλ€.
DBμμ μνλ λ§νΌλ§ μλΌμ€κΈ° μν΄μλ `offset`κ³Ό `limit`μ μ¬μ©νλλ°, `offset`μ 건λλΈ λ°μ΄ν°μ κ°μ, `limit`μ μλΌλΌ λ°μ΄ν°μ κ°μλ₯Ό λ§νλ€. νμ΄μ§λΉ λ°μ΄ν°λ₯Ό 10κ°μ© 보μ¬μ£Όλ νλ©΄μμ 2νμ΄μ§λ₯Ό μμ²νλ€λ©΄ 쿼리문μ `select * from table limit 10 offset 10`μ΄ λλ€. λλ `select * from table limit 10, 10`κ³Ό κ°μ΄ `limit`λ§μΌλ‘λ 쿼리문μ μμ±ν μ μλ€.
πSpringBoot - Spring Data JPAμ νμ΄μ§
JDBC λλ Mybatisλ₯Ό μ¬μ©νλ€λ©΄ νμ΄μ§λ₯Ό μ§μ 쿼리문μΌλ‘ ꡬνν΄μΌνμ§λ§, SpringBootμμλ Spring Data JPAλ₯Ό μ¬μ©νμ¬ νμ΄μ§μ κ΄ν μ²λ¦¬λ₯Ό `Pageable`κ³Ό `Page<E>`νμ μΌλ‘ λ€λ£¨κ³ μλ€.
`Pageable` νμ μ κ°μ²΄λ₯Ό ꡬμ±νμ¬ `repository` λ΄ λ©μλμ νλΌλ―Έν°λ‘ μ λ¬νλ©΄ κ²°κ³Όλ₯Ό `Page<T>`λ‘ λ°νλ°μ μ μλ€. `Pageable`λ μΈν°νμ΄μ€λ‘, νμ΄μ§ μμ± μ‘°κ±΄μ κ°μ§ κ°μ²΄λΌκ³ μκ°νλ©΄ λλ€. νμ΄μ§ λ²νΈ, κ°μ Έμ¬ λ°μ΄ν° κ°μ, μ λ ¬ λ°©μμ λ€μκ³Ό κ°μ΄ ꡬμ±ν μ μλ€.
// 1νμ΄μ§(0 ~ 10), bnoλ‘ λ΄λ¦Όμ°¨μ μ λ ¬
Pageable pageable = PageRequest.of(0, 10, Sort.by("bno").descending());
μ΄μ μ΄ `pageable`μ `repository`μ λ©μλμ νλΌλ―Έν°λ‘ μ λ¬νλ©΄ `Page<E>` κ°μ²΄λ₯Ό μ»μ μ μλ€.
// 1νμ΄μ§(0 ~ 10), bnoλ‘ λ΄λ¦Όμ°¨μ μ λ ¬
Pageable pageable = PageRequest.of(0, 10, Sort.by("bno").descending());
Page<Board> result = boardRepository.findAll(pageable);
μ½μμ κΉλ³΄λ©΄ Hibernateκ° λ€μκ³Ό κ°μ΄ 쿼리문μ μ€ννλ κ²μ νμΈν μ μλ€.
select
board0_.bno as bno1_0_,
board0_.modda t e as moddate2_0_,
board0_.re gdat e as regdate3_0_,
board0_.con t ent as content4_0_,
board0_. titl e as title5_0_,
board0_.wr it er as writer6_0_
from
board board0_
order by
board0_.bno desc limit ?
`Page<T>` νμ μ κ°μ²΄λ λ΄λΆμ μΌλ‘ νμ΄μ§ μ²λ¦¬μ νμν μ¬λ¬ μ 보λ₯Ό κ°μ§κ³ μλ€. μ 체 λ°μ΄ν°μ κ°μλ λͺ κ°μΈμ§, μ΄ νμ΄μ§λ λͺ κ°μΈμ§, λ°μ΄ν° κ°μλ λͺ κ°μΈμ§ λ±μ μ 보λ₯Ό λͺ¨λ κ°μ Έμ¬ μ μλ€.
Page<Board> result = boardRepository.findAll(pageable);
result.getTotalElements(); // μ΄ λ°μ΄ν° κ°μ: limit 쑰건μ κ±Έμ§ μμμ λ κ°μ
result.getTotalPage(); // μ΄ νμ΄μ§: μ¬μ΄μ¦μ λ§μΆ° μλΌλμ λ νμ΄μ§ κ°μ
result.getNumber(); // νμ΄μ§ λ²νΈ: κ°μ Έμ¨ νμ΄μ§
result.getSize(); // νμ΄μ§ λΉ κ°μ Έμ€λ λ°μ΄ν° κ°μ
result.getContent(); // κ°μ Έμ¨ λ°μ΄ν° λͺ©λ‘ List<Board>
`getTotalElements()`λ λ°μ΄ν° λͺ©λ‘μ κ°μ Έμ€λ κ²μ΄ μλλΌ limit 쑰건μ κ±Έμ§ μμμ λμ μ΄ λ°μ΄ν° κ°μλ₯Ό λ»νλ―λ‘ μ£Όμνλ€. λ°μ΄ν° λͺ©λ‘λ§ λ°λ‘ `List<T>` κ°μ²΄λ‘ μ»κ³ μΆλ€λ©΄ `getContent()`λ₯Ό μ¬μ©νλ©΄ λλ€.
πνμ©
νλ©΄μμ κ²μ 쑰건, νμ΄μ§λ₯Ό μμ²νμ λ κ²μ 쑰건μ λ§μ‘±ν νΉμ κΆνμ λ©€λ²λ₯Ό λ±λ‘μμΌλ‘ μ λ ¬ ν νμ΄μ§ μ²λ¦¬νμ¬ νλ©΄μ 보μ¬μ£Όκ³ μ νλ€. μ΄λ κ²μ 쑰건μ μ μ©νκΈ° μν΄ `queryDSL`μ μ¬μ©νλ€. κ²μ μ‘°κ±΄μ΄ μ‘΄μ¬νκ±°λ μ‘΄μ¬νμ§ μμ λ λͺ¨λ νμ΄μ§ μ²λ¦¬νμ¬ λ°μ΄ν°λ₯Ό κ°μ Έμ€κ³ μΆλ€λ©΄ `RepositoryImpl`μ λ€μκ³Ό κ°μ΄ μμ±νλ€.
package com.torobbb.repository.search;
import com.torobbb.domain.Member;
import com.torobbb.domain.MemberRole;
import com.torobbb.domain.QMember;
import com.querydsl.core.BooleanBuilder;
import com.querydsl.jpa.JPQLQuery;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.support.QuerydslRepositorySupport;
import java.util.List;
import java.util.Set;
public class MemberSearchImpl extends QuerydslRepositorySupport implements MemberSearch {
public MemberSearchImpl() {
super(Member.class);
}
@Override
public Page<Member> searchMember(String[] types, String keyword, Pageable pageable, Set<MemberRole> memberRoleSet) {
// QueryDSL κ°μ²΄
QMember member = QMember.member;
// select ... from member
JPQLQuery<Member> query = from(member);
// order by regDate desc;
query.orderBy(member.regDate.desc());
BooleanBuilder booleanBuilder = new BooleanBuilder();
if(types != null && keyword != null){
for(String type : types){
switch (type){
case "i" :
// where id like ...
booleanBuilder.or(member.id.contains(keyword));
break;
case "n" :
// or name like ...
booleanBuilder.or(member.name.contains(keyword));
break;
}
}// for
}// if
// and roleSet like
memberRoleSet.forEach(memberRole -> {
booleanBuilder.and(member.roleSet.contains(memberRole));
});
query.where(booleanBuilder);
// νμ΄μ§
this.getQuerydsl().applyPagination(pageable, query);
List<Member> list = query.fetch();
long count = query.fetchCount();
return new PageImpl<>(list, pageable, count);
}
}
`PageImpl<T>`λ `Page<T>` μΈν°νμ΄μ€μ ꡬν κ°μ²΄λ‘ μ€μ λͺ©λ‘ λ°μ΄ν°(List<T>), νμ΄μ§ μ 보(Pageable), μ 체 κ°μ(long)μ νλΌλ―Έν°λ‘ μ λ¬νλ©΄ `Page<T>` νμ μ μ°Έμ‘° λ³μλ‘ μ°Έμ‘°νμ¬ μ¬μ©ν μ μλ€.
ν΄λΉ `Repository`μ `searchMember()` λ©μλλ₯Ό μ¬μ©νλ `ServiceImpl`μ λ€μκ³Ό κ°λ€.
@Override
public PageResponseDTO<Member> findBySpecificRoles(Set<MemberRole> memberRoleSet, PageRequestDTO pageRequestDTO) {
Pageable pageable = pageRequestDTO.getPageable();
String[] types = pageRequestDTO.getTypes(); // split("_")
String keyword = pageRequestDTO.getKeyword();
log.info("types: " + Arrays.toString(types));
log.info("keyword: " + keyword);
// κ²μ μΉ΄ν
κ³ λ¦¬, ν€μλ, νΉμ κΆνμ κ°μ§ λ©€λ² regDate λ΄λ¦Όμ°¨μμΌλ‘ νμ΄μ§ μ²λ¦¬ν΄μ κ°μ Έμ΄
Page<Member> memberPage = memberRepository.searchMember(types, keyword, pageable, memberRoleSet);
// νμ΄μ§ μμ λ΄κΈ΄ dtoList
List<Member> dtoList = memberPage.getContent();
return PageResponseDTO.<Member>withAll()
.pageRequestDTO(pageRequestDTO)
.dtoList(dtoList)
// κ²μνμ λ μ΄ κ°μ(νμ΄μ§μ λ΄κΈ΄ κ°μ λ§κ³ κ²μ κ²°κ³Ό κ°μ)
.total((int)memberPage.getTotalElements())
.build();
}
μ€μ μ¬μ©ν λλ `Member` μν°ν°λ₯Ό λ°λ‘ λ°ννμ§ λ§κ³ `DTO`λ‘ λ³ννμ¬ λ°ννλλ‘ νλ€.
ν΄λΉ λ©μλλ₯Ό μ¬μ©νλ `Controller`λ λ€μκ³Ό κ°λ€.
@GetMapping("/member")
public String index(Model model, PageRequestDTO pageRequestDTO){
Set<MemberRole> memberRoleSet = new HashSet<>();
memberRoleSet.add(MemberRole.MEMBER);
PageResponseDTO<Member> pageResponseDTO = memberService.findBySpecificRoles(memberRoleSet, pageRequestDTO);
model.addAttribute("admin", MemberRole.MEMBER);
model.addAttribute("pageResponseDTO", pageResponseDTO);
return "/member/index";
}
`/member` urlμ μμ²νμ λ `MEMBER` κΆνμ κ°μ§ λ©€λ²μ 리μ€νΈλ₯Ό 보μ¬μ€λ€. `PageRequestDTO`μ `PageResponseDTO`λ νμ΄μ§ μ²λ¦¬λ₯Ό μν΄ λ·°μμ λͺ¨λ μ 보λ₯Ό νλ²μ μμ²νκ±°λ λ°±μμ λ·°λ‘ λκΈ°κΈ° μν κ°μ²΄λ‘, νμνλ€λ©΄ λ€μκ³Ό κ°μ΄ μ μΈνλ€.
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PageRequestDTO {
// κΈ°λ³Έκ°: nullμ΄λ©΄ 1νμ΄μ§
@Builder.Default
private int page = 1;
// κΈ°λ³Έκ°: nullμ΄λ©΄ 10κ°μ©
@Builder.Default
private int size = 10;
// κ²μ μΉ΄ν
κ³ λ¦¬
private String type;
// κ²μ ν€μλ
private String keyword;
private String link;
public Pageable getPageable(String...props){
return PageRequest.of(this.page -1, this.size, Sort.by(props).descending());
}
public String[] getTypes(){
if(type == null || type.isEmpty()){
return null;
}
return type.split("");
}
}
@Getter
@ToString
public class PageResponseDTO<E> {
private int page;
// νμ΄μ§λΉ κ°μ μμ² λ°λ‘ μμΌλ©΄ 10κ°μ©
@Builder.Default
private int size = 10;
private int total;
// μμ νμ΄μ§
private int start;
// λ
private int end;
private boolean prev;
private boolean next;
private List<E> dtoList;
// PageResponseDTO.<Member>withAll λ‘ μ¬μ©ν μ μμ
@Builder(builderMethodName = "withAll")
public PageResponseDTO(PageRequestDTO pageRequestDTO, List<E> dtoList, int total){
if(total <= 0){
return;
}
this.page = pageRequestDTO.getPage();
this.size = pageRequestDTO.getSize();
this.total = total;
this.dtoList = dtoList;
// νλ©΄μμ < 1 2 3 4 5 6 7 8 9 10 > μ΄λ κ² λ³΄μ¬μ£ΌλκΉ endλ λ§μ§λ§ μΆλ ₯λ νμ΄μ§ λ²νΌ, startλ μ²μ μΆλ ₯λ νμ΄μ§ λ²νΌ
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.start > 1;
this.next = total > this.end * this.size;
}
}
μ΄μ μ λ¬λ°μ `pageResponseDTO`λ₯Ό μ¬μ©ν΄ νλ©΄μ λ€μκ³Ό κ°μ΄ μμ±ν μ μλ€.
<div style="display: inline-flex">μ΄ [[${pageResponseDTO.total}]]λͺ
</div>
<div th:each="member : ${pageResponseDTO.getDtoList()}" th:with="admin=${admin}">
<img class="rounded-circle" th:src="@{/img/{fileName}(fileName=${member.getFileName()})}">
<div style="display: inline-flex" th:text="${member.name}"></div>
<div style="display: inline-flex" th:text="${#temporals.format(member.regDate, 'yy-MM-dd')}"></div>
<div class="float-end">
<button style="display: inline-flex" class="btn btn-secondary" th:data-name="${member.id}" th:onclick="adminUpdate(this.getAttribute('data-name'))">μμ </button>
<button style="display: inline-flex" class="btn btn-danger" th:data-name="${member.id}" th:onclick="admindelete(this.getAttribute('data-name'))">μμ </button>
</div>
</div>
<div class="">
<ul class="pagination flex-wrap">
<li class="page-item" th:if="${pageResponseDTO.prev}">
<a class="page-link" th:data-num="${pageResponseDTO.start -1}">μ΄μ </a>
</li>
<th:block th:each="i : ${#numbers.sequence(pageResponseDTO.start, pageResponseDTO.end)}">
<li th:class="${pageResponseDTO.page == i} ? 'page-item active' : 'page-item'">
<a class="page-link" th:data-num="${i}" >[[${i}]]</a>
</li>
</th:block>
<li class="page-item" th:if="${pageResponseDTO.next}">
<a class="page-link" th:data-num="${pageResponseDTO.end + 1}">λ€μ</a>
</li>
</ul>
</div>
κ²μ μμλ `pageRequestDTO` λ₯Ό μ λ¬ν μ μλλ‘ form νκ·Έλ₯Ό λ€μκ³Ό κ°μ΄ μΆκ°νλ€.
<form action="/member" method="get">
<input type="hidden" name="size" th:value="${pageRequestDTO.size}">
<div class="input-group">
<div class="input-group-prepend">
<select class="form-select type" name="type">
<option value="">---</option>
<option th:selected="${pageRequestDTO.type == 'n'}" value="n">μ΄λ¦</option>
<option th:selected="${pageRequestDTO.type == 'i'}" value="i">μμ΄λ</option>
<option th:selected="${pageRequestDTO.type == 'ni'}" value="ni">μ΄λ¦/μμ΄λ</option>
</select>
</div>
<input type="text" class="form-control keyword" name="keyword" th:value="${pageRequestDTO.keyword}">
<div class="input-group-append">
<button type="submit" class="btn btn-outline-primary searchBtn">κ²μ</button>
<button type="button" class="btn btn-outline-secondary clearBtn">μ΄κΈ°ν</button>
</div>
</div>
</form>
κ²μμ μ€ννλ©΄ `/member?size=10&type=n&keyword=μλ ` urlλ‘ μ΄λνλ€. type, keywordκ° μ‘΄μ¬νκΈ° λλ¬Έμ κ²μ μ‘°κ±΄μ΄ μ μ©λ νμ΄μ§κ° μ²λ¦¬λμ΄ λ·°μ λ°νλλ€.
πμ°Έκ³
μλ° μΉ κ°λ° μν¬λΆ - ꡬλ©κ°κ² μ½λ©λ¨
Page (Spring Data Core 3.2.3 API)
getTotalPages int getTotalPages() Returns the number of total pages. Returns: the number of total pages
docs.spring.io
Pageable (Spring Data Core 3.2.3 API)
isUnpaged default boolean isUnpaged() Returns whether the current Pageable does not contain pagination information. Returns:
docs.spring.io
'Java > Spring' μΉ΄ν κ³ λ¦¬μ λ€λ₯Έ κΈ
[SpringBoot] μ μ 리μμ€μ κΆν μꡬνμ§ μκΈ° (0) | 2024.03.28 |
---|---|
[SpringBoot] Spring Security: permitAll()μ΄ λμνμ§ μμ (0) | 2024.03.27 |
[Spring] 리λ€μ΄λ μ ν νμκ° λ무 λ§μ΅λλ€. (0) | 2024.03.24 |
[Spring] μ€νλ§ μν리ν°(Spring Security) (0) | 2024.03.21 |
[SpringBoot] Spring Security μ¬μ© μ deprecated (0) | 2024.03.16 |