프로젝트를 진행하던 중, 기존 페이징 처리가 되어있던 부분이 무한스크롤 방식으로 변경이 되었다.
때문에 사용자에게 페이지 번호를 전달 받을 필요가 없었고 Jpa의 Pageable을 사용할 이유가 없었기에 다른 방법을 모색하게 되었다.
Jpa Pageable 의 가장 큰 문제는 페이지 번호가 증가함에 따라 반환 시간이 증가한다는 점이다.
이는 Pageable이 Offset 방식으로 동작하기 때문인데, 페이지 번호가 증가할 수록 앞선 페이지의 데이터를 스캔하고 버려야하기에 일정한 반환 시간을 보장할 수 없게 된다.
아래는 Mysql 8.0 환경에 저장된 1100만 개의 데이터를 활용해 페이지 별 반환 시간을 테스트 해본 결과이다.
데이터는 페이지당 10개 씩 읽어오도록 하였다.
// 테스트 엔티티
@Entity
@Getter
@NoArgsConstructor
public class Customer {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Gender gender;
private String phone;
private String address;
private String visitPath;
private String photoUrl;
private String memo;
private LocalDate birthDate;
private CustomerStatus status;
@Builder
public Customer(String name, Gender gender, String phone, String address, String visitPath,
String photoUrl, String memo, LocalDate birthDate) {
this.name = name;
this.gender = gender;
this.phone = phone;
this.address = address;
this.visitPath = visitPath;
this.photoUrl = photoUrl;
this.memo = memo;
this.birthDate = birthDate;
this.status = CustomerStatus.ACTIVE;
}
}
// 사용 메소드
Page<Customer> pageable(Pageable pageable);
페이지 번호 평균 소요 시간
1099999 (10,999,990번째부터 조회) | 3.7s |
555555 (5,555,550번째부터 조회) | 2.2s |
0 (0번째부터 조회) | 42ms |
이러한 문제점은 Offset 으로 인해 발생하는 문제기에 offset 방식과 달리 불필요한 데이터는 스캔하지 않는 Keyset Pagination 방식을 활용하기로 하였다.
테스트는 위와 동일한 환경에서 진행하였다.
// 사용 쿼리
@Override
public List<Customer> keyset(int lastId, int size) {
QCustomer qCustomer = QCustomer.customer;
return queryFactory
.selectFrom(qCustomer)
.where(qCustomer.id.lt(lastId))
.orderBy(qCustomer.id.desc())
.limit(size)
.fetch();
}
페이지 번호 평균 소요 시간
1099999 (10,999,990번째부터 조회) | 635ms |
555555 (5,555,550번째부터 조회) | 582ms |
0 (0번째부터 조회) | 612ms |
해당 방식은 필요한 데이터의 인덱스 부터 조회하기에 일정한 반환 시간을 보장하게 된다.
하지만 Pageable과 달리 특정 페이지 이동이 어렵고,
정렬 기준이 되는 ID 값이 필수로 존재해야하며,
프론트에게 lastId 즉, 기준이 되는 값을 필수적으로 공유해야한다는 문제점도 존재한다.
이러한 문제점이 있음에도 무한스크롤이라는 요구사항에 기존 방식과 비교하여 확실한 성능 차이가 존재했기에 Keyset Pagination 방식을 채택하여 반영하게 되었다.
'기술적 고민' 카테고리의 다른 글
(MSA) 주문 및 결제에 이벤트 기반 SAGA 패턴을 활용한 이유 (0) | 2025.04.26 |
---|---|
동시성 제어에 캐시와 분산락 Redisson 을 도입한 이유 (1) | 2025.04.09 |
예약 관리 서비스에 RabbitMQ 를 도입하게 된 이유 (0) | 2025.03.26 |
데이터 유실 방지 - 외부 저장소(S3) 장애 대응 (0) | 2025.03.04 |
메타데이터를 이용한 객체 값 자동 생성 ( DatabaseMetaData ) (0) | 2024.10.08 |