Skip to content

무한 스크롤링과 페이지네이션

Minseong Park edited this page Sep 11, 2024 · 2 revisions

image

안녕하세요! 오늘은 무한 스크롤링과 페이지네이션, 그리고 SPOT! 팀의 커서 기반 페이지네이션 구현에 대해 이야기해보려고 합니다. 재미있고 유익한 내용이 될 거예요. 자, 시작해볼까요?

무한 스크롤링과 페이지네이션: 데이터를 우아하게 제공하는 방법

무한 스크롤링이란?

무한 스크롤링은 사용자가 페이지의 끝에 도달했을 때 추가 컨텐츠를 자동으로 로드하는 기술입니다. 인스타그램이나 페이스북 피드를 떠올려보세요. 스크롤을 계속 내리면 새로운 게시물이 계속해서 나타나죠? 바로 그게 무한 스크롤링이에요.

무한 스크롤링

페이지네이션이란?

페이지네이션은 대량의 정보를 여러 페이지로 나누어 제공하는 방식입니다. 구글 검색 결과 페이지 하단에 있는 숫자들, 그게 바로 페이지네이션이에요~!

image

SPOT! 팀의 페이지네이션 여정: offset에서 커서로

1차 MVP: offset 기반 페이지네이션

처음에 SPOT! 팀은 간단한 offset 기반 페이지네이션을 구현했습니다. 이 방식은 구현이 쉽고 직관적이에요.

SELECT * FROM reviews
ORDER BY date_time DESC
LIMIT 20 OFFSET 40;

이 쿼리는 "날짜순으로 정렬해서 40개를 건너뛰고 그 다음 20개를 가져와줘"라고 말하는 거죠.

Spring에서는 Pageable 인터페이스를 사용하면 아주 편리하게 offset 기반 페이지네이션을 구현할 수 있습니다. 예를 들어:

@Override
public Page<Review> findByStadiumIdAndBlockCode(
        Long stadiumId,
        String blockCode,
        Integer rowNumber,
        Integer seatNumber,
        Integer year,
        Integer month,
        Pageable pageable) {
    Page<ReviewEntity> reviewEntities =
            reviewCustomRepository.findByStadiumIdAndBlockCode(
                    stadiumId, blockCode, rowNumber, seatNumber, year, month, pageable);
    return reviewEntities.map(ReviewEntity::toDomain);
}

이 코드에서 Pageable 객체는 페이지 번호, 페이지 크기, 정렬 정보 등을 포함하고 있어, 개발자가 별도로 offset을 계산할 필요 없이 편리하게 페이지네이션을 구현할 수 있습니다.

2차 MVP: 커서 기반 페이지네이션으로의 진화

하지만 SPOT! 팀은 더 나은 방법을 찾아 커서 기반 페이지네이션으로 업그레이드했습니다. 왜 그랬을까요?

  1. 성능 향상
  • 데이터가 많아질수록 offset 방식은 느려지거든요. 반면에 커서 방식은 항상 일정한 성능을 유지해요!
  1. 일관성 & 실시간 데이터 처리에 적합함.
  • 사실 이 문제가 가장 큰 이유예요.
  • 예를 들어, A 유저가 1~10번까지 조회를 한 상황에서, 정렬 기준에 따라 다른 유저들이 리뷰 데이터를 계속 생성한다면, 유저는 같은 데이터만 계속 보일 수도 있는거에요.
  • 정리하자면, 데이터가 중간에 추가되거나 삭제되어도 커서 방식은 항상 일관된 결과를 보여주는거죠.
  • 따라서 커서방식은 새로운 리뷰가 계속 추가되는 상황에서 더 안정적이에요.

자, 이제 SPOT! 팀의 코드를 살펴볼까요?

public record BlockReviewRequest(
    @Parameter(description = "열 번호 (필터링)") Integer rowNumber,
    @Parameter(description = "좌석 번호 (필터링)") Integer seatNumber,
    @Min(1000) @Max(9999) @Parameter(description = "년도 (4자리 숫자)") Integer year,
    @Min(1) @Max(12) @Parameter(description = "월 (1-12)") Integer month,
    @Parameter(description = "다음 페이지 커서") String cursor,
    @Parameter(description = "정렬 기준", example = "DATE_TIME") SortCriteria sortBy,
    @Parameter(description = "페이지 크기") Integer size) {}

BlockReviewRequest는 정말 스마트하네요! 커서뿐만 아니라 필터링 옵션도 함께 제공하고 있어요. 사용자는 특정 좌석이나 날짜의 리뷰만 볼 수도 있겠어요.

그리고 이 코드를 보세요:

private BooleanExpression getCursorCondition(SortCriteria sortBy, String cursor) {
    if (cursor == null) {
        return null;
    }

    String[] parts = cursor.split("_");

    LocalDateTime dateTime;
    Long id;
    switch (sortBy) {
        case LIKES_COUNT:
            if (parts.length != 3) return null;
            int likeCount = Integer.parseInt(parts[0]);
            dateTime = LocalDateTime.parse(parts[1]);
            id = Long.parseLong(parts[2]);
            return reviewEntity
                    .likesCount
                    .lt(likeCount)
                    .or(
                            reviewEntity
                                    .likesCount
                                    .eq(likeCount)
                                    .and(
                                            reviewEntity
                                                    .dateTime
                                                    .lt(dateTime)
                                                    .or(
                                                            reviewEntity
                                                                    .dateTime
                                                                    .eq(dateTime)
                                                                    .and(
                                                                            reviewEntity.id.lt(
                                                                                    id)))));
        case DATE_TIME:
        default:
            if (parts.length != 2) return null;
            dateTime = LocalDateTime.parse(parts[0]);
            id = Long.parseLong(parts[1]);
            return reviewEntity
                    .dateTime
                    .lt(dateTime)
                    .or(reviewEntity.dateTime.eq(dateTime).and(reviewEntity.id.lt(id)));
    }
}

이 코드는 정렬 기준에 따라 다른 커서 로직을 적용하고 있어요.

  • LIKES_COUNT 정렬의 경우: 좋아요 수_날짜_ID 형식의 커서를 사용합니다.
  • DATE_TIME 정렬의 경우: 날짜_ID 형식의 커서를 사용합니다.

이렇게 하면 동일한 좋아요 수나 날짜가 있더라도 항상 일관된 순서로 데이터를 가져올 수 있어요.

성능 관점에서의 비교

특성 Offset 방식 커서 방식
구현 복잡도 낮음 (Spring의 Pageable 사용 시 매우 간단) 중간 (커서 로직 구현 필요)
성능 데이터 증가에 따라 성능 저하 일정한 성능 유지
일관성 데이터 변경 시 결과 불일치 가능 항상 일관된 결과 제공
임의 페이지 접근 가능 어려움
전체 데이터 개수 파악 쉬움 어려움

정렬 기준에 따라 다른 커서 로직을 사용하고, 복합 조건을 통해 정확성을 보장하고 있습니다. 이는 대규모 데이터를 다루는 실제 서비스에서 매우 중요한 부분이죠.

자, 이제 여러분도 무한 스크롤링과 페이지네이션, 그리고 커서 기반 페이지네이션의 강력함을 이해하셨길 바랍니다. 다음에도 SPOT! 팀의 재미있는 기술 이야기로 찾아올게요! 😊

SPOT! 내가 만드는 야구장 시야 서비스

SPOT! 다운로드 👇🏻

SPOT! - 내가 만들어가는 야구장 시야 서비스 - Apps on Google Play