@Override
    public Slice<MemberSimpleResponseDto> getFollowersLegacy(Pageable pageable, String memberId, String prevMemberId) {
        List<MemberSimpleResponseDto> content = jpaQueryFactory.select(Projections.constructor(MemberSimpleResponseDto.class,
                        member.memberId,
                        member.nickname,
                        member.profileImage,
                        member.feature
                )).from(follow)
                .join(member)
                .on(follow.from.eq(member))
                .where(follow.to.memberId.eq(memberId))
                .orderBy(member.memberId.desc())
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize()+1)
                .fetch();

        boolean hasNext = content.size() > pageable.getPageSize(); // 뒤에 더 있는지 확인
        content = hasNext ? content.subList(0, pageable.getPageSize()) : content; // 뒤에 더 있으면 1개 더 가져온거 빼고 넘긴다
        return new SliceImpl<>(content, pageable, hasNext);
    }
@Override
public Slice<MemberSimpleResponseDto> getFollowers(Pageable pageable, String memberId, String prevMemberId) {
    List<MemberSimpleResponseDto> content = jpaQueryFactory.select(Projections.constructor(MemberSimpleResponseDto.class,
            member.memberId,
            member.nickname,
            member.profileImage,
            member.feature
            )).from(follow)
            .join(member)
            .on(follow.from.eq(member))
            .where( ltMemberId(prevMemberId),
                    follow.to.memberId.eq(memberId)
            ).orderBy(member.memberId.desc())
            .limit(pageable.getPageSize()+1)
            .fetch();

    boolean hasNext = content.size() > pageable.getPageSize(); // 뒤에 더 있는지 확인
    content = hasNext ? content.subList(0, pageable.getPageSize()) : content; // 뒤에 더 있으면 1개 더 가져온거 빼고 넘긴다
    return new SliceImpl<>(content, pageable, hasNext);
}

		private BooleanExpression gtMemberId(String prevMemberId) {
        if (prevMemberId == null) return null; // prevMember가 주어지지 않으면 null로 where 조건 무시됨
        return member.memberId.gt(prevMemberId); //마지막으로 리턴된 prevMemberId 다음부터 탐색
    }
@Test
    @Transactional
    void noOffset(){
        // memberId가 멤버99가 멤버 989보다 뒤에있다. 그러나 이건 제대로 된 결과이다! PK 기준으로 정렬하는 것은 가능하다
        Long srt = System.currentTimeMillis();
        FollowerRequestDto req = FollowerRequestDto.builder().pageNo(98).pageSize(10).build();
        Slice<MemberSimpleResponseDto> legacy =
                followRepository.getFollowersLegacy(PageRequest.of(req.getPageNo(), req.getPageSize()), "멤버1000", req.getPrevMemberId());
        log.info("legacy : {}",System.currentTimeMillis() - srt);
        log.info("{}", legacy.getContent().get(9).getMemberId());
        assertThat(legacy.getContent().size()).isEqualTo(10);

        Long srt2 = System.currentTimeMillis();
        Slice<MemberSimpleResponseDto> noOffset =
                followService.getFollower(
                        FollowerRequestDto.builder().pageNo(0).pageSize(10).prevMemberId("멤버990").build(), "멤버1000");
        log.info("noOffset : {}",System.currentTimeMillis() - srt2);
        log.info("{}", noOffset.getContent().get(0).getMemberId());
        assertThat(noOffset.getContent().size()).isEqualTo(10);
    }
2024-01-23T16:23:45.197+09:00  INFO 16816 --- [           main] c.a.c.member.service.FollowServiceTest   : legacy : 482ms
2024-01-23T16:23:45.199+09:00  INFO 16816 --- [           main] c.a.c.member.service.FollowServiceTest   : noOffset : 2ms

참고한 향로님 블로그