Pagination
언버스의 페이지네이션을 설명합니다.
1. 개요
1.1. 기존 페이징 방식
언버스의 페이지네이션을 설명하기 이전에, 기존 대부분의 프로젝트에서 사용하는 방식들을 알아보고, 언버스의 페이지네이션을 살펴보도록 합니다.
1.1.1. 페이징 정보를 파라미터로 받아서 처리
@RequestMapping(value="/shop/list")
public @ResponseBody ModelMap itemListIndex(ModelMap model,
@RequestParam int firstIndex,
@RequestParam int recordCountPerPage) {
/** ---------------------------------------------- paging 로직 **/
Map param = new HashMap();
param.put("firstIndex", firstIndex);
param.put("firstIndex", recordCountPerPage);
int totalItemCnt = searchService.searchProductItemCount(param);
model.addAttribute("totalItemCount", totalItemCnt);
/** --------------------------------------------// paging 로직 **/
/** 실제 business logic **/
List<ProductVO> list = searchService.searchProductItem(param);
model.addAttribute("list", list);
return model;
}
실제 business logic 보다 페이징을 위한 코드가 더 많습니다.
1.1.2. 파라미터 모델을 만들어 사용
@RequestMapping(value="/shop/list")
public @ResponseBody ModelMap itemListIndex(ModelMap model, Product product) {
/** ---------------------------------------------- paging 로직 **/
int totalItemCnt = searchService.searchProductItemCount(product);
model.addAttribute("totalItemCount", totalItemCnt);
/** --------------------------------------------// paging 로직 **/
/** 실제 business logic **/
List<ProductVO> list = searchService.searchProductItem(product);
model.addAttribute("list", list);
return model;
}
// Product.java
@Data
public class Product {
private Long id;
private String name;
//.....
/** ---------------------------------------------- paging 파라미터 **/
private int firstIndex = 1;
private int recordCountPerPage = 10;
/** --------------------------------------------// paging 파라미터 **/
}
여전히 페이징을 위한 부분이 들어가며, Product 도메인 클래스는 자신의 고유 속성과 관련 없는 페이징 속성을 가져야 합니다.
Unvus Pagination
Resource
@GetMapping(value = "/user", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<List<User>> listUser(@RequestParam(value="q", required = false) Map<String, Object> q) throws URISyntaxException {
if(q == null) q = new HashMap();
List<User> userList = userService.listUser(q);
/** 페이징을 위한, 전체 수, 현재 페이지 등의 정보를 자동으로 담아줍니다. **/
HttpHeaders headers = PaginationHeaderUtil.generatePaginationHttpHeaders();
return new ResponseEntity<>(userList, headers, HttpStatus.OK);
}
Service
public List<User> listUser(Map<String, Object> params) {
// 검색 조건 세팅. 이 부분은 `검색 조건` 섹션에서 자세히 다루도록 합니다.
QueryBuilder qb = QueryBuilder.of(params, UserDsl.defaultAlias());
// with pagination
List<User> lists = InPagination.withRequest((p) -> userRepository.listUser(qb.build()));
return lists;
}
Unvus Pagination 은:
- 도메인객체, 파라미터 등에 페이징 관련 부분을 모두 없애고, 비즈니스 로직에 집중
- 페이징을 위한 count 쿼리 또한 자동으로 실행 (count 쿼리 id 는 list 쿼리명 뒤에 'Cnt' suffix - 변경 가능)
참고
pagination 은 크게 아래 두가지로 사용법으로 구분됩니다.
사용법 | 설명 | 참고 |
---|---|---|
InPagination.page() | request, response 와 관계 없이, 비즈니스로직 내에서 페이징이 필요한 경우 사용합니다. | 1. 일반 페이징 & 정렬 |
InPagination.sort() | request, response 와 관계 없이, 비즈니스로직 내에서 소트가 필요한 경우 사용합니다. | 1. 일반 페이징 & 정렬 |
InPagination.withRequest() | request 로 부터 페이지 정보를 얻고, 결과 값을 response 에 담습니다. | 2. Primary 페이징 & 정렬 |
1. 일반 페이징 & 정렬
비즈니스 로직상에서 페이징, 정렬이 필요한 경우 Pagination 을 사용 하는 방법을 설명합니다.
1.1. 페이징
java 비즈니스 로직에서 특정 쿼리에 페이징이 필요한 경우 다음과 같이 사용 할 수 있습니다.
// 처음 5개의 항목 가져오기
List<User> lists = InPagination.page((p) -> userRepository.listUser(param), 5);
다음처럼 offset 구현을 할 수 있습니다.
// 3번째 페이지의 5개 항목 가져오기
List<User> lists = InPagination.page((p) -> userRepository.listUser(param), 3, 5);
1.2. 정렬
java 비즈니스 로직에서 특정 쿼리에 정렬이 필요한 경우 다음과 같이 사용 할 수 있습니다.
// user_id 오름차순으로 데이터를 가져옵니다. 페이징 안함
List<User> lists = InPagination.sort((p) -> userRepository.listUser(param), new SortBy("u.user_id"));
다음과 같이, 여러가지 정렬 기준을 설정 할 수 있습니다.
// 정렬 조건은 여러개가 될 수 있습니다
// user_id 오름차순, user_name 내림차순 으로 데이터를 가져옵니다. 페이징 안함
List<User> lists = InPagination.sort(
(p) -> userRepository.listUser(param),
new SortBy("u.user_id"),
new SortBy("u.user_name", SortBy.SortDirection.DESC));
정렬에 대한 옵션을 지정 할 수 있습니다. InPagination.SortByType.REPLACE
는 기존 정렬이 있을 경우, 이를 무시하고 대체하도록 합니다.
// 정렬 조건은 여러개가 될 수 있습니다
// user_id 오름차순, user_name 내림차순 으로 데이터를 가져옵니다. 페이징 안함
List<User> lists = InPagination.sort(
(p) -> userRepository.listUser(param),
InPagination.SortByType.REPLACE,
new SortBy("u.user_id"));
정렬 옵션은 REPLACE_IF_NOT_EXISTS
, REPLACE
, APPEND
가 있으며, 차이점은 다음과 같습니다. 이 정렬 옵션은 1. 일반 페이징 & 정렬 에서 보다는, 주로 2. Primary 페이징 & 정렬 에서 기본정렬값을 설정하기 위해 사용합니다.
옵션 | 설명 |
---|---|
APPEND | 정렬 순서를 추가합니다. 이전에 설정된 정렬 순서가 있다면 그 뒤로 추가합니다. |
REPLACE | 정렬 순서를 설정합니다. 이전에 설정된 정렬 순서가 있다면 무시하고 덮어 씁니다. |
REPLACE_IF_NOT_EXISTS | 이전에 설정된 정렬 순서가 없는 경우에만, 정렬 순서를 설정합니다. |
1.3. 복합 (페이징 + 정렬)
다음과 같이 페이징과 정렬을 구성 할 수 있습니다.
// user_id 오름차순, 3번째 페이지의 5개의 데이터만 가져옵니다
InPagination.pageSort((p) -> userRepository.listUser(param), 3, 5, new SortBy("u.user_id"));
2. Http Request 로부터 페이징 & 정렬
InPagination.withRequest()
는 HttpServletRequest
파라미터에서 pagination 정보를 가져와서 설정하고, 그 실행 결과를 HttpServletResponse
header 에 설정합니다.
기본 사용법은 다음과 같습니다.
List<User> lists = InPagination.withRequest((p) -> userRepository.listUser(qb.build()));
기본 소팅을 추가해주고 싶으면 다음과 같이 합니다.
List<Member> lists = InPagination.withRequest(
(p) -> {
return memberRepository.listMember(qb.build());
},
new SortBy(u.renderColumn(u.id), SortBy.SortDirection.DESC)
);
프레임워크에서 페이징을 위해 HttpServletRequest
파라미터에서 획득하는 값은 다음과 같습니다.
파라미터 | 설명 | 기본값 | 예시 |
---|---|---|---|
currentPage | 요청 하는 페이지 번호 | 1 | currentPage=8 |
dataPerPage | 한 페이지당 데이터 수 | 10 | dataPerPage=50 |
linkPerPage | UI 상 화면에 보여질 페이지 링크 수 | 10 | linkPerPage=5 |
sortBy | 쿼리 정렬 기준(콤마 구분) | - | sortBy=u.user_id:desc,u.user_name:asc |
sortByList | 쿼리 정렬 기준(파라미터 배열 구분) | - | sortByList=u.user_id:desc&sortByList=u.user_name:asc |
페이징 결과 값은 HttpServletResponse
의 header 에 다음과 같은 키값으로 설정됩니다.
x-nv-total-count
: 전체 데이터 수x-nv-data-count
: 한 페이지당 데이터 수x-nv-page
: 현재 페이지 번호
이로써, 응답받은 페이지(jsp), 또는 ajax client 에서 결과에 따른 페이지 UI 화면을 구성 할 수 있게 합니다.
RestAPI 경우 **Resource
에서 값을 반환하기 전에, 다음과 같이 PaginationHeaderUtil.generatePaginationHttpHeaders()
를 호출 함으로서, header 에 결과값을 세팅 할 수 있습니다.
public ResponseEntity<> some() {
/** some logic here **/
HttpHeaders headers = PaginationHeaderUtil.generatePaginationHttpHeaders();
return new ResponseEntity<>(userList, headers, HttpStatus.OK);
}
3. 전제조건
TIP
여기에서 설명하는 페이징(및 소팅)을 위한, 기본 구조 및 설정은 code genie 에 의해 자동으로 생성됩니다.
페이징(및 소팅)을 위해서는 다음과 같은 조건이 성립되어야 합니다.
3.1. count 쿼리 제공
프레임워크에서는 기본적으로, list query 의 mapper id 에 Cnt
를 붙인 mapper id 를 count 쿼리로 판단합니다.
즉, 페이징 설정을 한 뒤 userRepository.listUser(param)
를 호출하면, 프레임워크 내부에서는 userRepository.listUserCnt(param)
를 함께 호출 하게 됩니다.
그러므로, 프레임워크에게 count 쿼리를 함께 제공해주어야 합니다.
code genie 를 통해 code 를 생성하면, list 쿼리와 함께 다음과 같이 count 쿼리도 함께 생성해줍니다.
TIP
count 맵퍼 아이디로 mapper id + Cnt 외에, 다른 아이디를 사용하고 싶다면, 아래 3.2. @Pageable 어노테이션 의 countMapperId 설명을 참고해주세요
<select id="listUser" resultMap="userResult">
SELECT *
FROM user u
<!-- 검색 조건 sql include -->
<include refid="userCondition">
<property name="_alias" value="u."/>
</include>
<!-- 정렬 logic -->
<include refid="Common.sort"/>
<!-- 페이징 query logic -->
<include refid="Common.pagingFooter"/>
</select>
<select id="listUserCnt" resultType="int">
SELECT COUNT(*)
FROM user u
<!-- 검색 조건 sql include -->
<include refid="userCondition">
<property name="_alias" value="u."/>
</include>
</select>
3.2. @Pageable 어노테이션
페이징(소팅)이 작동되기 원하는 mapper method 에는 다음과 같이 @Pageable
어노테이션이 있어야 합니다. 해당 어노테이션이 존재 하지 않으면 InPagination.**
을 통해 페이징 설정을 하더라도, 작동되지 않습니다.
@Pageable
List<Member> listMember(Map<String, Object> params);
int listMemberCnt(Map<String, Object> params);
속성 | 설명 | 기본값 |
---|---|---|
countMapperId | list mapper id + Cnt 가 아닌 별도의 count mapper id 를 제공 | - |
usePagingKey | 페이징 사용 여부 키값 | UnvusConstants.USE_PAGING |
useMergeQuery | merge query 를 사용할지 여부 | false |
mergeMapperId | merge query 사용시 merge query mapper id | - |
mergeParamId | merge query 사용시 id 리스트의 param 명 | ids |
4. merge query
쿼리 결과가 1:N 형태를 가질때, 페이징을 하면, 기대했던 수량 보다 더 작은 데이터를 가져오게 됩니다. 다음 예로 살펴보겠습니다.
User (사용자) 클래스가 있습니다. User 는 여러개의 Role (권한) 을 가질 수 있습니다.
public class User {
private String acntId;
private String name;
private List<Role> roleList;
}
Role (권한) 클래스는 다음과 같습니다.
public class Role {
private String code;
private String name;
}
이와 같이 user 와 role 는 1:N 관계로 구성되어 있습니다.
쿼리를 실행해보겠습니다.
SELECT u.user_acnt_id, u.user_name, r.role_code
FROM nv_user u
JOIN nv_role r ON u.user_id = r.user_key
결과는 다음과 같습니다.
user_acnt_id | user_name | role_code |
---|---|---|
guavatak | 구아바 | ROLE_ADMIN |
guavatak | 구아바 | ROLE_CMS |
admin | 관리자 | ROLE_ADMIN |
admin | 관리자 | ROLE_AGENT |
user | 사용자 | ROLE_USER |
위 결과는 mybatis 에 의해 3개의 User 객체(guavatak, admin, user) 를 반환하게 됩니다. | ||
그리고, 각 객체는 자신의 roleList 를 보유한 상태가 됩니다. |
이런 경우에 3개만 가져오도록 paging 을 하면 다음과 같은 결과가 나옵니다.
SELECT u.user_acnt_id, u.user_name, r.role_code
FROM nv_user u
JOIN nv_role r ON u.user_id = r.user_key
LIMIT 3
결과는 다음과 같습니다.
user_acnt_id | user_name | role_code |
---|---|---|
guavatak | 구아바 | ROLE_ADMIN |
guavatak | 구아바 | ROLE_CMS |
admin | 관리자 | ROLE_ADMIN |
이는 mybatis 에 의해 2개의 User 객체(guavatak, admin) 를 반환하게 되며, admin 은 ROLE_AGENT 가 없는 상태가 되어버립니다.
이런 경우에는 merge query 를 사용하도록 합니다.
merge query 를 사용하지 않을때는 다음과 같은 순서로 쿼리를 실행하게 됩니다. (리스트 mapper id 가 listUser 라고 가정)
- listUser
- listUserCnt
merge query 를 사용하면, 앞에 한 단계가 추가됩니다.
- listUserIds
- listUser
- listUserCnt
이 경우 Ids
쿼리는 다음과 같은 형태가 됩니다.
-- listUserIds
SELECT DISTINCT u.user_acnt_id
FROM nv_user u
JOIN nv_role r ON u.user_id = r.user_key
LIMIT 3
결과는 다음과 같습니다.
user_acnt_id |
---|
guavatak |
guavatak |
admin |
admin |
user |
이 결과값은 리스트쿼리의 파라미터(ids) 로 전달 되며, 리스트 쿼리는 ids 파라미터가 있는 경우 다른 조건을 생략하고 해당 조건만 실행하게 됩니다.
-- listUser
SELECT u.user_acnt_id, u.user_name, r.role_code
FROM nv_user u
JOIN nv_role r ON u.user_id = r.user_key
WHERE u.user_acnt_id IN ('guavatak', 'admin', 'user')
결과는 다음과 같습니다.
user_acnt_id | user_name | role_code |
---|---|---|
guavatak | 구아바 | ROLE_ADMIN |
guavatak | 구아바 | ROLE_CMS |
admin | 관리자 | ROLE_ADMIN |
admin | 관리자 | ROLE_AGENT |
user | 사용자 | ROLE_USER |
이렇게 해서 가져오고 싶은 수 만큼 정확히 가져 올 수 있게됩니다.
TIP
merge query 에서 사용하는 Ids
mapper sql 또한 code genie
에 의해 자동으로 생성되어 집니다.
5. 예제 코드
다음은 상품 목록
화면에 Top banner 3개
와, 최근 본 상품 5개
를 함께 보여주는 Controller class 예제 코드 입니다.
@GetMapping("/product")
public String listProduct(Map<String, Object> q, Model model) {
// "Top banner" 를 위해, 5개의 데이터만 가져옴
List<Banner> bannerList = InPagination.pageSort((p) -> bannerService.listTopBanner(q), 5);
model.addAttribute("bannerList", bannerList);
// "최근 본 상품" 을 위해, 5개의 데이터만 가져옴
List<Product> recentList = InPagination.pageSort((p) -> productService.listRecentProduct(q), 5);
model.addAttribute("recentList", recentList);
// 이 메소드의 Main 인 "상품 목록" 을 위해, http request parameter 부터 페이징(소팅) 설정
List<Product> productList = InPagination.withRequest((p) -> productService.listProduct(q));
model.addAttribute("productList", productList);
return "product-list";
}