<aside>
이중 연결리스트는 아래와 같이 이전 연결 리스트와 이후 연결리스트 아이디를 가져야 한다.
만약 그렇다면 이 경우에 페이징을 어떻게 해서 가지고 올 것 인가?
@Entity
@Getter
@Setter(AccessLevel.NONE)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(
indexes = {
@Index(name = "idx_play_list_id", columnList = "playListId"),
}
)
public class DoubleLinkedListItem {
@Id @SnowflakeId
@GeneratedValue
@Column(name = "doubleLinkedListItemId")
private Long id;
@Column(unique = true)
@Comment("이전 연결리스트 아이디")
private Long prevId;
@Column(unique = true)
@Comment("이후 연결리스트 아이디")
private Long nextId;
@Column(name = "playListId")
private Long playListId;
}
</aside>
<aside>
@Repository
public interface TestDoubledListRepository extends JpaRepository<DoubleLinkedListItem, Long> {
@Query(value = """
WITH RECURSIVE chain AS (
SELECT d.double_linked_list_item_id,
d.prev_id,
d.next_id,
d.play_list_id,
0 AS depth
FROM double_linked_list_item d
WHERE d.double_linked_list_item_id = :cursorId
AND d.play_list_id = :playListId
UNION ALL
SELECT d.double_linked_list_item_id,
d.prev_id,
d.next_id,
d.play_list_id,
c.depth + 1
FROM double_linked_list_item d
JOIN chain c
ON d.double_linked_list_item_id = c.next_id
WHERE d.play_list_id = :playListId
AND c.depth < :pageSize - 1
)
SELECT double_linked_list_item_id,
prev_id,
next_id,
play_list_id
FROM chain
ORDER BY depth
LIMIT :pageSize
""", nativeQuery = true)
List<DoubleLinkedListItem> findNextPage(Long playListId,
Long cursorId,
int pageSize);
}
cursorId ──▶ 첫 줄(앵커) ──▶ 재귀로 next_id 따라가기 ──▶ depth<pageSize 까지 모으기
(depth = 0) (depth = 1,2,3 …) (최대 pageSize-1 단계)
WITH RECURSIVE는 재귀적 방식을 의미한다.
즉, 임시 테이블을 하나 만들고 하나씩 가지고 와서 임시테이블이 채운다.
이 과정에서 가지고 온 데이터를 토대로 JOIN 연산이 들어간다.
왜 depth라는 값이 필요할까? depth의 값이 없다면 재귀적 호출을 다음 연결리스트가 없을때 까지 진행된다.
즉, 나는 20개의 데이터만 필요한데 모든 데이터가 임시 테이블로 올라오는 것이다.
이는 결국 CPU와 I/O의 오버헤드가 발생하는 것이다. 따라서 depth를 통해 최대 가지고 오는 수를 제한하는 것이다.
</aside>
<aside>
gap-based-numbering 방식은 순서를 주면서 각 순서 사이에 간격을 주는 것이다.
즉, [0, 10000, 20000, 30000] 이런식의 간격을 준다.
이렇게 주는 이유는 20000을 0과 10000 사이로 옮길때 둘 사이의 평균 값인 5000으로 바꾸는 것이다.
즉, 20000을 0과 10000 사이로 옮긴다면 [0, 5000, 10000, 30000] 이 되는 것이다.
@Entity
@Getter
@Setter(AccessLevel.NONE)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(
uniqueConstraints = {
@UniqueConstraint(
name = "uk_playlist_position",
columnNames = { "playListId", "position" }
)
}
)
public class GapOrderingItem {
@Id @SnowflakeId
@GeneratedValue
@Column(name = "gapOrderingItemId")
private Long id;
@Column(nullable = false)
@Comment("정렬용 숫자 값으로 같은 플레이리스트 내에서 유일해야 한다")
private Long position;
@Column(name = "playListId")
private Long playListId;
}
</aside>
<aside>
@Repository
@RequiredArgsConstructor
public class TestGapPaginationRepository {
private final JPAQueryFactory query;
public List<GapOrderingItem> getItemByCursor(
Long playListId,
Long cursor
) {
return query.select(gapOrderingItem)
.from(gapOrderingItem)
.where(
getId(playListId),
getAfter(cursor)
)
.orderBy(gapOrderingItem.position.asc())
.limit(MAX_PLAY_PAGE_SIZE + 1)
.fetch();
}
private BooleanExpression getAfter(Long cursor) {
return cursor == null ? null : gapOrderingItem.position.gt(cursor);
}
private BooleanExpression getId(Long playListId) {
return playListId == null ? null : gapOrderingItem.playListId.eq(playListId);
}
}
즉, 위와같이 훨씬 깔끔한 쿼리로 성능을 테스트할 수 있다
<aside>
사용자의 수를 5분동안 5000명으로 증가시킨다.
각 요청은 특정 playListId를 하나 랜덤으로 고른 후 거기서 특정 페이지를 가지고 와 반환한다
</aside>
<aside>
항목 | 결과 |
---|---|
총 요청 수 | 745,226 |
총 테스트 시간 | 5분 1.7초 (약 301.7초) |
평균 처리량(Throughput) | 약 2,470 req/s |
평균 HTTP 요청 응답 시간 | 512.11 ms |
요청 실패율 | 0.00% (0건 실패) |
</aside> |