Chewing 페이징 방식 개선일지

Crow·2023년 1월 5일
0

사용할 스택은 Spring boot에서 가장 많이 사용되는 JPA의 구현체인 Hibernate & 쿼리 작성을 위해서 Querydsl사용함
현재 적용 할 수 있는 페이징은 Covering Index, NoOffset 방식 존재

Before

repository

package io.web.chewing.repository;

import io.web.chewing.domain.booking.Booking;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface BookingRepository extends JpaRepository<Booking, Long> {

    Optional<Booking> findByIdAndMember_Nickname(Long id, String nickname);
    /*List<BookingDTO> findBookingListPaging(String store, String storeName);*/

    Page<Booking> findAllByMember_Nickname(Pageable pageable, String nickname);
}

After

1. CoveringIndex

repository

public List<BookingPaginationDto> paginationCoveringIndexByEntityToDto(String name, int pageNo, int pageSize) {
        // 1) 커버링 인덱스로 대상 조회
        List<Long> ids = queryFactory.select(booking.id)
                .from(booking)
                .where(booking.member.nickname.eq(name))
                .orderBy(booking.id.desc())
                .limit(pageSize)
                .offset((long) pageNo * pageSize)
                .fetch();
        // 1-1) 대상이 없을 경우 추가 쿼리 수행 할 필요 없이 바로 반환
        if (CollectionUtils.isEmpty(ids)) {
            return new ArrayList<>();
        }

        // 2)
        return queryFactory.select(Projections.fields(BookingPaginationDto.class,
                        booking.id.as("id"),
                        booking.real_name,
                        booking.people,
                        booking.date,
                        booking.time,
                        booking.member.nickname.as("member_nickname"),
                        booking.store.name.as("store_name"),
                        booking.bookingState))
                .from(booking)
                .innerJoin(booking.store, store)
                .innerJoin(booking.member, member)
                .where(booking.id.in(ids))
                .orderBy(booking.id.desc())
                .fetch();
    }

testcode

    @Test
    public void CoveringIndexByEntityToDto() {
        // given

        String searchName = "testnick10k";
        //when
        List<BookingPaginationDto> bookings = bookingCustomRepository.paginationCoveringIndexByEntityToDto(searchName, 50000, 10);

        //then
        assertThat(bookings).hasSize(10);
    }

2. NoOffset(cursor)

repository

    @Test
    public void NoOffsetFirstPage() {
        //given
        String searchNick = "testnick10k";

        //when
        List<BookingPaginationDto> bookings = bookingCustomRepository.paginationNoOffset(null, searchNick, 10);

        //then
        assertThat(bookings).hasSize(10);
    }

testcode

    @Test
    public void NoOffsetFirstPage() {
        //given
        String searchNick = "testnick10k";

        //when
        List<BookingPaginationDto> bookings = bookingCustomRepository.paginationNoOffset(null, searchNick, 10);

        //then
        assertThat(bookings).hasSize(10);
    }
    
        @Test
    public void NoOffsetSecondPage() {
        //given
        String searchName = "testnick10k";

        //when
        List<BookingPaginationDto> bookings = bookingCustomRepository.paginationNoOffset(9970L, searchName, 10);

        //then
        assertThat(bookings).hasSize(10);
    }

3. Legacy to Querydsl

repository

public List<BookingPaginationDto> paginationLegacy(String searchName, int pageNo, int PageSize) {
        return queryFactory.select(Projections.fields(BookingPaginationDto.class,
                        booking.id.as("id"),
                        booking.real_name,
                        booking.people,
                        booking.member.nickname.as("member_nickname"),
                        booking.store.name.as("store_name"),
                        booking.date,
                        booking.time,
                        booking.member.nickname,
                        booking.bookingState))
                .from(booking)
                .where(
                        booking.member.nickname.eq(searchName)
                )
                .orderBy(booking.id.desc())
                .limit(PageSize)
                .offset((long) pageNo * PageSize)
                .fetch();
    }

testcode

    @Test
    public void legacy() throws Exception {
        //given
        String searchName = "testnick10k";

        //when
        List<BookingPaginationDto> bookings = bookingCustomRepository.paginationLegacy(searchName, 50000, 10);

        //then
        assertThat(bookings).hasSize(10);
    }


데이터 100만건 기준으로 속도를 junit 테스트 실행 결과 다음과 같은 차이가 나왔다.

첫 배포 때 보다 테스트 환경에서 1초 정도의 의미있는 차이를 냈다

하지만 첫 배포시엔 JpaRepository 인터페이스에 기본 crud 메서드로 쿼리 효율같은걸 생각하지 않고 사용했기 때문에 이런 큰 차이가 났다고 생각한다.


#피드백

이걸 개선하면서 내 단점인 너무 미래의 상황까지 고려해서 생각한다는 점을 다시 상기했다.

따라서 너무 먼 미래를 보고 설계하기 보단 가까운 미래까지만 고려해서 설계를 해야 한다는 경험을 얻은거 같다.

그리고 내용 정리를 할때 좀 보기 쉽게 할 수 있도록 다른 분들의 글을 더 많이 참고하고 읽어봐야겠다


출처
https://jojoldu.tistory.com/528
https://www.youtube.com/watch?v=zMAX7g6rO_Y

profile
어제보다 개발 더 잘하기 / 많이 듣고 핵심만 정리해서 말하기 / 도망가지 말기 / 깃허브 위키 내용 가져오기

2개의 댓글

comment-user-thumbnail
2023년 1월 16일

create table persistent_logins
(
username varchar(64) not null ,
series varchar(64) primary key ,
token varchar(64) not null ,
last_used timestamp not null
);

1개의 답글