대용량 데이터를 한 번에 전송하면 성능 저하와 사용자 경험 문제를 야기할 수 있습니다. 따라서 클라이언트에게 필요한 일부 데이터만 전달하는 페이지네이션은 매우 중요한 기능입니다. Spring Data JPA는 기본적으로 Pageable 인터페이스를 제공하여 쉽게 페이지네이션을 구현할 수 있도록 지원합니다.
요청에서 페이지 번호, 페이지 크기, 정렬 기준(sortBy)과 정렬 방향(direction)을 별도로 받아 명확한 API 계약을 정의합니다.
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class PageableRequestDTO {
@Min(0)
private int page = 0;
@Min(1)
private int size = 10;
// 정렬 기준 필드 (예: "name", "id", "address")
private String sortBy;
// 정렬 방향: ASC 또는 DESC. 기본값은 ASC
private Sort.Direction direction = Sort.Direction.ASC;
}
응답에서는 데이터 목록과 함께 페이지 관련 메타데이터(현재 페이지 번호, 페이지 크기, 전체 요소 수, 전체 페이지 수, 마지막 페이지 여부)를 포함합니다.
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PageableResponseDTO<T> {
private List<T> content;
private int pageNumber;
private int pageSize;
private long totalElements;
private int totalPages;
private boolean last;
// Static factory 메서드를 통해 Page 객체를 DTO로 변환
public static <S, T> PageableResponseDTO<T> from(Page<S> page, Function<S, T> converter) {
List<T> content = page.getContent().stream()
.map(converter)
.collect(Collectors.toList());
return PageableResponseDTO.<T>builder()
.content(content)
.pageNumber(page.getNumber())
.pageSize(page.getSize())
.totalElements(page.getTotalElements())
.totalPages(page.getTotalPages())
.last(page.isLast())
.build();
}
}
요청 DTO에서 sortBy와 direction을 분리하면 다음과 같은 장점이 있습니다:
서비스 레이어에서는 이 DTO를 사용해 PageRequest를 생성합니다.
Sort sort = Sort.unsorted();
if (pageableRequestDTO.getSortBy() != null && !pageableRequestDTO.getSortBy().isEmpty()) {
sort = Sort.by(pageableRequestDTO.getDirection(), pageableRequestDTO.getSortBy());
}
Pageable pageable = PageRequest.of(pageableRequestDTO.getPage(), pageableRequestDTO.getSize(), sort);
위의 static factory 메서드 from을 활용하면, 서비스 레이어에서 페이지 네비게이션 정보를 가진 Page 객체를 DTO로 쉽게 변환할 수 있습니다. 이 메서드는 두 가지 제네릭 타입 <S, T>를 사용합니다.
메서드 내부에서 수행되는 작업은 다음과 같습니다:
page.getContent()로 가져온 리스트를 스트림으로 변환하고, 전달된 converter 함수를 적용하여 DTO 리스트로 변환합니다.PageableResponseDTO<T> 객체로 생성합니다.전체 코드는 아래와 같습니다:
public static <S, T> PageableResponseDTO<T> from(Page<S> page, Function<S, T> converter) {
List<T> content = page.getContent().stream()
.map(converter)
.collect(Collectors.toList());
return PageableResponseDTO.<T>builder()
.content(content)
.pageNumber(page.getNumber())
.pageSize(page.getSize())
.totalElements(page.getTotalElements())
.totalPages(page.getTotalPages())
.last(page.isLast())
.build();
}
이렇게 분리하면, 서비스 레이어는 단순히 Page 객체와 엔티티를 DTO로 변환하는 함수를 전달하는 역할만 하게 되어, 책임이 명확하게 분리됩니다.
명확한 이름 부여와 의도 표현:
객체 생성 로직의 캡슐화:
유연한 객체 생성:
가독성과 유지보수성 향상:
서비스 레이어의 최종 코드는 다음과 같이 간결해집니다:
public PageableResponseDTO<CompanyResponseDTO> getCompanies(PageableRequestDTO pageableRequestDTO) {
Sort sort = Sort.unsorted();
if (pageableRequestDTO.getSortBy() != null && !pageableRequestDTO.getSortBy().isEmpty()) {
sort = Sort.by(pageableRequestDTO.getDirection(), pageableRequestDTO.getSortBy());
}
Pageable pageable = PageRequest.of(pageableRequestDTO.getPage(), pageableRequestDTO.getSize(), sort);
Page<Company> companiesPage = companyRepository.findAll(pageable);
return PageableResponseDTO.from(companiesPage, CompanyResponseDTO::fromEntity);
}
여기서 CompanyResponseDTO::fromEntity는 Company 엔티티를 DTO로 변환하는 메서드입니다. 이와 같이 static factory 메서드를 활용하면, 코드가 더 읽기 쉽고 유지보수하기 용이해집니다.
sortBy와 direction을 분리하여 클라이언트가 명확한 정렬 정보를 전달하도록 설계