목록을 보여줄때 무한 스크롤 방식으로 보여주는것이 사용성 면에서 좋을 것 같아 모든 목록 조회는 무한 스크롤 방식을 사용하기로 결정했습니다. 무한 스크롤 방식을 구현하기 전에 Offset 페이징No offset 페이징 기법의 차이에 대해 간단히 알아보겠습니다.

📃 페이징 기법

Offset

Offset과 Limit을 활용한 페이징 기법의 경우 다음과 같은 형태로 쿼리가 생성됩니다.

SELECT ...
FROM ...
WHERE ...
ORDER BY id DESC
OFFSET {page_number}
LIMIT {page_size}

이경우 page_number에 해당하는 행만큼 데이터를 읽어들인 후 다시 page_size만큼의 행을 읽고 앞에 읽은 행을 삭제하는 과정을 거치게 되는데, 데이터가 많아 질수록 읽어야 할 데이터의 개수가 많아지게 되고 결국 데이터 베이스에 많은 부하를 주게 됩니다.

또한 새로운 페이지를 가져오는 중간에 새로운 행이 삽입되면 새로운 페이지에는 이전 페이지와 중복되는 행이 존재하게 됩니다.

image

첫 페이지에서는 10번 Row 까지 조회한 후 다음 페이지를 조회하기 전, 새로운 행이 삽입됩니다. offset을 기준으로 다음 페이지의 행을 조회하게 되기 때문에 10번 Row는 다음페이지에서도 조회되는 중복조회 현상이 발생할 수 있습니다.

No Offset

만약 Offset을 사용하지 않는다면, 마지막 조회한 행의 id값을 기준으로 읽지 않은 행을 page size만큼 조회하면 됩니다. 이를 No Offset방식이라고 하며 id라는 클러스터 인덱스를 사용하기 때문에 시작부분을 빠르게 찾아 조회가 가능합니다.

No offset 방식의 쿼리 예시

SELECT ...
  FROM ...
 WHERE ...
   AND id < ?last_seen_id
 ORDER BY id DESC
 FETCH FIRST 10 ROWS ONLY

페이지 수에 따른 응답시간 비교

8517F1DF-5539-4F22-9C06-B600D402DA96

Offset방식을 사용하였을때는 Page의 수가 늘어남에 따라 응답시간이 느려지는것을 볼 수 있지만, 인덱스(id)를 기준으로 정렬하고 조회하는경우 첫페이지를 읽는것과 동일한 속도로 응답하는것을 확인할 수 있습니다.

성능면에서는 이러한 장점이 있지만 순차적으로만 다음페이지를 조회할 수 있다는 단점 또한 존재합니다.

결론 : 무한 스크롤 방식에서는 페이지 버튼이 필요 없기 때문에 성능상 우수한 no offset방식으로 구현하는것이 적절해 보입니다.

💻 구현

BoardQueryRepository구현

@Repository
@RequiredArgsConstructor
public class BoardQueryRepository {

  private final JPAQueryFactory jpaQueryFactory;

  public List<Board> getSliceOfBoard(
      @Nullable
      Long id,
      int size,
      @Nullable
      String keyword
  ) {
    return jpaQueryFactory.selectFrom(board)
                          .where(ltBoardId(id), search(keyword))
                          .orderBy(board.id.desc())
                          .limit(size)
                          .fetch();
  }

  @SuppressWarnings("all")
  private BooleanExpression search(String keyword) {
    return containsTitle(keyword).or(containsDescription(keyword));
  }

  private BooleanExpression containsTitle(@Nullable String title) {
    return hasText(title) ? board.title.contains(title) : null;
  }

  private BooleanExpression containsDescription(@Nullable String description) {
    return hasText(description) ? board.description.contains(description) : null;
  }

  private BooleanExpression ltBoardId(@Nullable Long id) {
    return id == null ? null : board.id.lt(id);
  }

}

첫 요청

[
    {
        "id": 42,
        "title": "Lodge",
        "description": "Malawi matrix Cambridgeshire"
    },

    ...

    {
        "id": 33,
        "title": "Avon",
        "description": "synergies Money complexity generation"
    }
]

2번째 요청

[
    {
        "id": 32,
        "title": "holistic",
        "description": "analyzing Fresh Senior facilitate Practical"
    },

    ...

    {
        "id": 23,
        "title": "Bedfordshire",
        "description": "Jewelery Pants Liechtenstein Metal South"
    }
]

검색어를 포함한 요청

[
    {
        "id": 23,
        "title": "Bedfordshire",
        "description": "Jewelery Pants Liechtenstein Metal South"
    },
    {
        "id": 19,
        "title": "Oklahoma",
        "description": "primary Pants"
    },
    {
        "id": 13,
        "title": "Pants",
        "description": "B2C Harbors Kuwaiti"
    }
]

⌨️ 코드

Spring-Board/BoardQueryRepository.java at main · waterfogSW/Spring-Board · GitHub

☎️ Reference

+ Recent posts