[Spring] 페이징 적용하기

장숭혁·2024년 8월 29일
1

TIL작성

목록 보기
57/60

페이지네이션(Pagenation)을 적용하는 이유

  • 대량의 데이터를 효율적으로 처리하기 위함이다. List로 가져올 경우 성능에 부정적 영향을 끼친다. 일정한 크기로 잘라서 가져옴으로써 성능을 향상시킨다.

  • 네트워크 트래픽 절약 일정한 크기로 가져옴으로써 트래픽을 절약한다.

  • 사용자 경험 개선 : 빠른 로딩, 탐색 용이

..... 등등이 있다.

페이지 네이션을 적용하는 방법

Spring Data JPA는 PageablePage를 제공한다.

Pageable 인터페이스

  • getPageNumber() : 현재 페이지 번호 반환(0부터 시작한다.) -> 1부터 시작한다고 생각하면 잘못된 데이터를 가져올 수 있다.

  • getPageSize() : 한 페이지당 최대 항목 수 반환

  • getOffset() : 현재 페이지의 시작 위치를 반환한다.( 현재 페이지를 3으로 설정하고 pagesize를 15로 한다면 이 메소드는 45를 반환할 것이다. )

  • getSort() : 정렬 정보를 반환한다.

  • next() : 다음 페이지 정보를 반환한다.

  • previousOrFirst() : 이전 페이지 정보를 반환한다.

next()와 previousOrFirst()는 왜 필요한가?

  • next() 사용
Pageable firstPage = PageRequest.of(0, 10); // 첫 번째 페이지 0
Pageable previousPageableFirst = firstPage.previousOrFirst(); // 첫 번째 페이지
  • previousOrFirst() 사용
Pageable current = PageRequest.of(1, 10); // 현재 페이지 1
Pageable previousPageable = current.previousOrFirst(); // 이전 페이지는 0

새로운 Pageable 정보를 생성하지 않고도 간단하게 이전 혹은 다음 페이지로 넘어가게 한다.
-> 코드의 양을 줄일 수 있다.

각 메소드 코드 예시

import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;

public class PageableExample {
    public static void main(String[] args) {
        // 현재 페이지 번호: 0 (첫 페이지), 페이지 크기: 10
        Pageable pageable = PageRequest.of(0, 10, Sort.by("createdAt").descending());
        
        // 각 메서드의 사용 예시
        int pageNumber = pageable.getPageNumber();
        int pageSize = pageable.getPageSize();
        long offset = pageable.getOffset();
        Sort sort = pageable.getSort();
        Pageable nextPageable = pageable.next();
        Pageable previousPageable = pageable.previousOrFirst();

        // 출력 예시
        System.out.println("Page Number: " + pageNumber);  // 0
        System.out.println("Page Size: " + pageSize);      // 10
        System.out.println("Offset: " + offset);           // 0
        System.out.println("Sort: " + sort);               // createdAt: DESC
        System.out.println("Next Page Number: " + nextPageable.getPageNumber());  // 1
        System.out.println("Previous Page Number: " + previousPageable.getPageNumber()); // 0
    }
}

Page 인터페이스

Page 인터페이스는 페이징된 결과 데이터를 포함하는 객체를 나타냄

  • getContent() : 현재 페이지의 데이터 반환

  • getTotalElements() : 전체 데이터 수를 반환

  • getTotalPages() : 전체 페이지 수 반환

  • getNumber() : 현재 페이지 번호 반환

  • getSize() : 한 페이지당 항목 수 반환

  • hasNext() : 다음 페이지 있는지 여부 반환

  • hasPrevious() : 이전 페이지 있는지 여부 반환

Spring Framework 사용 예시

  • Controller
@GetMapping
    public ResponseEntity<PageDto> getAllProducts(Pageable pageable) {
        PageDto products = productService.getAllSearch(pageable);
        return ResponseEntity.ok(products);
    }
  • Service
public PageDto getAllSearch(Pageable pageable) {
        Pageable sortedPageable = PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(),
            Sort.by("productName").descending());
        Page<Product> products = productRepository.findAll(sortedPageable);

        var data = products.getContent().stream()
            .map(ProductSingleResponse::new)
            .toList();

        return new PageDto(data,
            products.getTotalElements(),
            products.getTotalPages(),
            pageable.getPageNumber(),
            data.size()
        );
    }
  • Repository
@Repository
public interface ProductRepository extends JpaRepository<Product, UUID> {

    Page<Product> findAll(@Nullable Pageable pageable);

    Page<Product> findByProductNameContaining(String search, Pageable pageable);
}
  • dto
public record PageDto(
    List<?> data,
    long totalElement,
    long totalPage,
    int currentPage,
    int size
) {
}
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class ProductSingleResponse {
    private Product product;
}
  • entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "products")
public class Product extends TimeStamped {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private UUID productId;

    @Column(nullable = false)
    private String productName;

    @Column
    private String description;

    @Column(nullable = false)
    private int price;

    @Column(nullable = false)
    private boolean isPublic = true;

    @Column(nullable = false)
    private boolean isDeleted = false;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id")
    private Order order;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "shop_id", referencedColumnName = "shopId")
    private Store store;

    @Builder
    public Product(String productName, String description, int price, Store store) {
        this.store = store;
        this.productName = productName;
        this.description = description;
        this.price = price;
    }
}

@Setter
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class TimeStamped {
    // Audit 필드
    // 모든 테이블에 created_at, created_by, updated_at, updated_by, deleted_at, deleted_by 필드를 추가하여 데이터 감사 로그 기록
    @CreatedDate
    @Column
    private LocalDateTime createdAt;

    @CreatedBy
    @Column
    private String createdBy;

    @LastModifiedDate
    @Column
    private LocalDateTime updatedAt;

    @LastModifiedBy
    @Column
    private String updatedBy;

    @Column
    private LocalDateTime deletedAt;

    @Column
    private String deletedBy;

}

코드 설명

  • Pageable은 매개변수로서 받을 수 있다 @RequestParam으로 받는것과 같다.
    -> 예시
@GetMapping
    public ResponseEntity<ApiResponse> getPaymentsByUserId(@RequestParam UUID userId,
        @RequestParam int page,
        @RequestParam int size,
        @RequestParam(required = false) String sortBy) {
        Pageable pageable = PageRequest.of(page, size, sortBy == null ? Sort.by("createdAt").descending() : Sort.by(sortBy));
        Page<Payment> paymentPage = paymentService.getPaymentsByUserId(userId, pageable);
        return ResponseEntity.ok(new ApiResponse(
            200, "success", "사용자 결제 내역 조회 성공", paymentPage));
    }
  • Page 인터페이스는 페이징된 결과 데이터를 포함하는 객체를 나타낸다. 그러므로
@RequestParam int page,
@RequestParam int size

매개변수에 각각 ?page=0&size=10 를 집어넣는다면 Page<> 객체에 그 부분에 해당되는 결과 리스트가 저장된다.

var data = products.getContent().stream()
            .map(ProductSingleResponse::new)
            .toList();

는 각각의 Product엔티티를 리스트로 담는데 다시 이를 PageDto에 List로 담음으로써 0페이지에 10개의 결과만 나오게 한다. ProductSingleResponse는 Product 엔티티 밖에 없으므로 왜 있는 것인지 의문을 가질 수 있으나 Product를 Product에 다시 매핑할 수 없어서 만들었다.

결과 예상

{
  "data": [
    {
      "productId": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
      "productName": "연어 초밥",
      "description": "신선한 연어로 만든 초밥입니다.",
      "price": 15000,
      "isPublic": true,
      "isDeleted": false,
      "createdAt": "2024-08-25T10:30:00",
      "createdBy": "manager",
      "updatedAt": "2024-08-27T15:45:00",
      "updatedBy": "manager",
      "deletedAt": null,
      "deletedBy": null
    },
    {
      "productId": "e2f3g4h5-6789-01bc-defg-2345678901bc",
      "productName": "장어 초밥",
      "description": "달콤한 양념으로 구운 장어 초밥입니다.",
      "price": 20000,
      "isPublic": true,
      "isDeleted": false,
      "createdAt": "2024-08-26T11:00:00",
      "createdBy": "manager",
      "updatedAt": "2024-08-27T16:00:00",
      "updatedBy": "manager",
      "deletedAt": null,
      "deletedBy": null
    }
  ],
  "totalElements": 5,
  "totalPages": 3,
  "currentPage": 0,
  "currentSize": 2
}
profile
코딩 기록

0개의 댓글

관련 채용 정보