저번 포스팅에서 공통 반환 dto를 생성 할 때 제네릭 타입으로 선언해둬서 반환으로 원하는 모든 타입을 넣을 수 있다.
그렇다면 입맛대로 새로 만든 리스폰스 클래스도 반환타입으로 지정이 가능하다는건데
그 점에서 페이지네이션 기능이 담긴 dto를 만들어서 해당 기능이 필요할 땐 해당 클래스를 사용해 써보도록 하겠다.
먼저 dto 클래스를 하나 생성한다
SearchPageResponse.class
public record SearchPageResponse<T>(
Long totalElements,
Long totalPages,
Integer currentPage,
Integer pageSize,
List<T> content) {}
record는 자바14쯤에서 새롭게 나온 클래스 타입인데, record 타입으로 선언하면 효율적이고 간결하게 dto 생성이 가능하다, getter, setter는 물론이고 생성자, toString같은 메서드, 그리고 불변성 보장도 해주다 보니 여러모로 유용하다
물론 lombok을 사용해도 되긴하지만 코드량을 좀 더 줄이고 간단하게 만들기위해 record로 생성했다.
다시 본론으로 와서 페이지네이션 기능은 99% 사용자의 편의성을 위해 만들어진 기능인데 이에따라 사용자와 좀 더 가까운곳에서 개발하는 프론트엔드쪽에서 활용해야한다고 생각한다.
그렇기때문에 만약 프론트엔드 개발자와 협업중이라면 서로 어떤 값들이 필요한지 문서화를 미리미리 해두는게 좋다.
여기서는 최대한 보편적인 페이지네이션 클래스로 생성하여
totalElements 에는 전체 데이터의 개수 (토탈페이지와 연관, 또는 전체 데이터개수 반환 필요할 때)
totalPages 에는 전체 페이지의 수
currentPage 는 현재 페이지
pageSize 는 페이지당 담길 개수
content 는 담겨있는 정보
정도를 넣었고 객체 생성을 위해 빌더 메서드 하나만 생성하도록 하겠다.
생성자가 자동으로 들어가서 굳이 안넣어도 되지만.. 그냥 다른곳들은 빌더로 생성을 하는데 여기만 생성자로 하면 코드가 좀 일관성 없어보여서 넣는다.
@Builder
public record SearchPageResponse<T>(
Long totalElements,
Long totalPages,
Integer currentPage,
Integer pageSize,
List<T> content) {
public static <T> SearchPageResponse<T> of(
Long totalElements,
Long totalPages,
Integer currentPage,
Integer pageSize,
List<T> content
) {
return SearchPageResponse.<T>builder()
.totalElements(totalElements)
.totalPages(totalPages)
.currentPage(currentPage)
.pageSize(pageSize)
.content(content)
.build();
}
}
그러면 일단 이런 형태가 완성이 되는데, 원래 dto마다 전부 테스트를 돌릴 생각은 없지만 해당 dto는 앞으로도 자주 사용할 코드기에 테스트코드로 한번 돌려보자
테스트 코드
class SearchPageResponseTest {
@Test
void 정상_데이터_생성_테스트() {
// Given
Long totalElements = 100L;
Long totalPages = 10L;
Integer currentPage = 1;
Integer pageSize = 10;
List<String> content = Arrays.asList("Item1", "Item2", "Item3");
// When
SearchPageResponse<String> response = SearchPageResponse.of(
totalElements,
totalPages,
currentPage,
pageSize,
content
);
// Then
assertNotNull(response);
assertEquals(totalElements, response.totalElements());
assertEquals(totalPages, response.totalPages());
assertEquals(currentPage, response.currentPage());
assertEquals(pageSize, response.pageSize());
assertEquals(content, response.content());
assertEquals(3, response.content().size());
}
@Test
void 빈_콘텐츠_생성_테스트() {
// Given
Long totalElements = 0L;
Long totalPages = 0L;
Integer currentPage = 1;
Integer pageSize = 10;
List<String> content = List.of();
// When
SearchPageResponse<String> response = SearchPageResponse.of(
totalElements,
totalPages,
currentPage,
pageSize,
content
);
// Then
assertNotNull(response);
assertEquals(totalElements, response.totalElements());
assertEquals(totalPages, response.totalPages());
assertEquals(currentPage, response.currentPage());
assertEquals(pageSize, response.pageSize());
assertTrue(response.content().isEmpty());
}
@Test
void Null_생성_테스트() {
// When
SearchPageResponse<String> response = SearchPageResponse.of(
null,
null,
null,
null,
null
);
// Then
assertNotNull(response);
assertNull(response.totalElements());
assertNull(response.totalPages());
assertNull(response.currentPage());
assertNull(response.pageSize());
assertNull(response.content());
}
}
실제 사용은 현재 프로젝트엔 없지만 예전에 했던 프로젝트 코드를 보면
controller
@Auth
@GetMapping("memo-list")
@Operation(summary = "메모 리스트 조회", description = "작성된 메모 리스트를 조회합니다.\n"
+ "empIdx 필드를 null로 주시면 전체 조회입니다.")
public Response<SearchPageResponse<MemoItem.Response>> getMemoList(
@RequestParam(name = "pageSize") Integer pageSize,
@RequestParam(name = "pageNumber") Integer pageNumber,
@RequestParam(name = "empIdx", required = false) Long empIdx
) {
return new Response<>(HttpStatus.OK.value(), memoService.getCallMemoList(pageSize, pageNumber-1, empIdx));
}
service
public SearchPageResponse<MemoItem.Response> getCallMemoList(Integer pageSize, Integer pageNumber, Long empIdx) {
Employee employee = employeeRepository.findByIdx(AuthHolder.getUserId())
.orElseThrow(() -> new CustomException(EmployeeErrorCode.NOT_FOUND_EMPLOYEE));
Pageable pageable = Pageable.ofSize(pageSize).withPage(pageNumber);
List<Memo> memos = memoRepository.findMemoList(pageable, empIdx, employee);
List<MemoItem.Response> memoItems = memos.stream()
.map(MemoItem.Response::from)
.toList();
Long total = memoRepository.countMemoList(empIdx, employee);
Long totalPage = (long)Math.ceil((double)total / pageSize);
return SearchPageResponse.of(total, totalPage, pageNumber + 1, pageSize, memoItems);
}
이런 느낌으로 사용하면 된다