대량의 데이터를 효율적으로 처리하기 위함이다. List로 가져올 경우 성능에 부정적 영향을 끼친다. 일정한 크기로 잘라서 가져옴으로써 성능을 향상시킨다.
네트워크 트래픽 절약 일정한 크기로 가져옴으로써 트래픽을 절약한다.
사용자 경험 개선 : 빠른 로딩, 탐색 용이
Spring Data JPA는 Pageable
과 Page
를 제공한다.
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 인터페이스는 페이징된 결과 데이터를 포함하는 객체를 나타냄
getContent() : 현재 페이지의 데이터 반환
getTotalElements() : 전체 데이터 수를 반환
getTotalPages() : 전체 페이지 수 반환
getNumber() : 현재 페이지 번호 반환
getSize() : 한 페이지당 항목 수 반환
hasNext() : 다음 페이지 있는지 여부 반환
hasPrevious() : 이전 페이지 있는지 여부 반환
@GetMapping
public ResponseEntity<PageDto> getAllProducts(Pageable pageable) {
PageDto products = productService.getAllSearch(pageable);
return ResponseEntity.ok(products);
}
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
public interface ProductRepository extends JpaRepository<Product, UUID> {
Page<Product> findAll(@Nullable Pageable pageable);
Page<Product> findByProductNameContaining(String search, Pageable pageable);
}
public record PageDto(
List<?> data,
long totalElement,
long totalPage,
int currentPage,
int size
) {
}
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class ProductSingleResponse {
private Product product;
}
@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;
}
@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));
}
@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
}