TIL_20250318_Pageable

Kim jisu·2025년 3월 18일

TIL

목록 보기
20/43

1. Pageable을 쓰는 이유

대용량 데이터를 한 번에 전송하면 성능 저하와 사용자 경험 문제를 야기할 수 있습니다. 따라서 클라이언트에게 필요한 일부 데이터만 전달하는 페이지네이션은 매우 중요한 기능입니다. Spring Data JPA는 기본적으로 Pageable 인터페이스를 제공하여 쉽게 페이지네이션을 구현할 수 있도록 지원합니다.


2. 요청과 응답을 위한 DTO 사용

2.1. PageableRequestDTO

요청에서 페이지 번호, 페이지 크기, 정렬 기준(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;
}

2.2. PageableResponseDTO

응답에서는 데이터 목록과 함께 페이지 관련 메타데이터(현재 페이지 번호, 페이지 크기, 전체 요소 수, 전체 페이지 수, 마지막 페이지 여부)를 포함합니다.

@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();
    }
}

3. 정렬 기준과 정렬 방향의 분리

요청 DTO에서 sortBy와 direction을 분리하면 다음과 같은 장점이 있습니다:

  • 명확한 API 계약: 클라이언트는 어떤 필드를 기준으로 정렬할지, 그리고 정렬 방향은 어떻게 할지를 명확히 전달할 수 있습니다.
  • 유효성 검증: 각 필드에 대해 별도의 유효성 검증을 적용할 수 있어 파라미터 오류를 줄일 수 있습니다.
  • 확장성: 미래에 정렬 조건을 확장하거나 추가 옵션을 도입할 때 DTO 구조 변경이 용이합니다.

서비스 레이어에서는 이 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);

4. PageableResponseDTO에 Static Factory 메서드 적용

위의 static factory 메서드 from을 활용하면, 서비스 레이어에서 페이지 네비게이션 정보를 가진 Page 객체를 DTO로 쉽게 변환할 수 있습니다. 이 메서드는 두 가지 제네릭 타입 <S, T>를 사용합니다.

  • S: Page 객체 내의 원본 데이터 타입 (예: Entity)
  • T: 변환 후의 DTO 타입

메서드 내부에서 수행되는 작업은 다음과 같습니다:

  1. 내용 변환:
    page.getContent()로 가져온 리스트를 스트림으로 변환하고, 전달된 converter 함수를 적용하여 DTO 리스트로 변환합니다.
  2. 빌더 패턴 활용:
    변환된 내용과 페이지 정보를 Lombok의 Builder 패턴을 사용하여 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로 변환하는 함수를 전달하는 역할만 하게 되어, 책임이 명확하게 분리됩니다.

Static factory 메서드를 사용하는 주요 이유

명확한 이름 부여와 의도 표현:

  • 생성자 대신 정적 팩토리 메서드를 사용하면, 메서드 이름을 통해 객체 생성의 의도와 역할을 명확하게 전달할 수 있습니다. 예를 들어, PageableResponseDTO.from(...)라고 이름을 지으면, "Page 객체로부터 PageableResponseDTO를 생성한다"는 의미가 명확해집니다.

객체 생성 로직의 캡슐화:

  • 정적 팩토리 메서드를 사용하면, 복잡한 객체 생성 로직(예: 엔티티를 DTO로 변환하고 페이지네이션 정보를 설정하는 과정)을 한 곳에 모아 관리할 수 있습니다. 이렇게 하면 서비스 레이어에서는 단순히 메서드를 호출하기만 하면 되어, 코드의 중복을 줄이고 유지보수가 용이해집니다.

유연한 객체 생성:

  • 정적 팩토리 메서드는 같은 타입의 인스턴스를 여러 방식으로 생성할 수 있는 유연성을 제공합니다. 캐싱, 서브타입 반환, 또는 조건에 따라 다른 인스턴스를 반환하는 등의 전략을 쉽게 구현할 수 있습니다.

가독성과 유지보수성 향상:

  • 생성자에 복잡한 로직을 넣는 대신, 명시적인 팩토리 메서드를 사용함으로써 코드의 가독성이 높아지고, 변경 사항이 있을 때 해당 메서드만 수정하면 되므로 유지보수성이 향상됩니다.

5. 전체 사용 방법

서비스 레이어의 최종 코드는 다음과 같이 간결해집니다:

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 메서드를 활용하면, 코드가 더 읽기 쉽고 유지보수하기 용이해집니다.


6. 마치며

  • DTO 사용: 요청과 응답에 각각 별도의 DTO를 사용하여 API 계약을 명확히 함
  • 정렬 기준과 방향 분리: sortBy와 direction을 분리하여 클라이언트가 명확한 정렬 정보를 전달하도록 설계
  • PageableResponseDTO의 static factory 메서드: 페이지네이션 관련 정보를 한 곳에 모아 책임 분리를 달성
profile
Dreamer

0개의 댓글