본문 바로가기
SpringBoot

페이징 개선(첫 페이지 조회결과 cache)

by se0nghyun2 2024. 4. 18.
첫 페이지 조회 결과 cache 하기

 


기존)

BookRepositoryImpl

@Repository
@RequiredArgsConstructor
public class BookRepositoryImpl implements BookRepository {

    private final JPAQueryFactory jpaQueryFactory;
    private final SpringDataJpaBookRepository bookRepository;

    ...
    
    @Override
    public Page<BookSearchResponseDto.Response> findBooksBySimpleCategory(InquiryCategory category, String inquiryWord, Pageable pageable) {
        JPQLQuery<BookSearchResponseDto.Response> query=jpaQueryFactory.select(
                    new QBookSearchResponseDto_Response(bookEntity.bookCode,bookEntity.bookName,bookEntity.bookAuthor,bookEntity.pubDt,bookEntity.bookState,bookEntity.bookImage)
                )
                .from(bookEntity)
                .where(getSearchCategory(category,inquiryWord))
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
        ;
        
        //데이터 조회
        List<BookSearchResponseDto.Response> content = query.fetch();

        //총 갯수
        long totalCount = query.fetchCount();

        return new PageImpl<>(content,pageable,totalCount);
    }
 	...
 }

 

해당 코드로 매번 페이지 변경 시마다  매번 2 번의 쿼리가 나가게 된다.

1. 데이터 조회 쿼리

2. count 조회 쿼리

 

매번 데이터조회쿼리는 그렇다치지만 매번 count조회쿼리가 나가는 것이 불편했다. 

첫 조회 시 totalCount를 알고 있다면 그 이외 페이징에선 굳이 알아야할 필요가 없어보였다.

 


개선) 첫 페이지 조회 결과 cache하기

 

https://jojoldu.tistory.com/531?category=637935

 

1. 첫 조회 시 totalCount 구하기 -> totalCount는 클라이언트단에서 저장 / 데이터 및 count 조회쿼리 발생
2. 그 이후 페이지 변경 시, 첫 조회 시 구한 totalCount를 포함시켜 요청 / 데이터 조회 쿼리 발생

 

쉽게 말하면, 첫번째 페이지 조회 시에만 totalCount 조회하고 그 이후 페이징 버튼에선 조회하지 않도록 하는 것이다.

해당 방식 사용 이유는 실시간으로 데이터 삽입이 일어나지 않았으며, 색 및 페이지버튼 이벤트가 모두 골고루 발생하였기에 사용하였다.  

 

BookRepositoryImpl

@Repository
@RequiredArgsConstructor
public class BookRepositoryImpl implements BookRepository {

    private final JPAQueryFactory jpaQueryFactory;
    private final SpringDataJpaBookRepository bookRepository;

    ...
    
    //클라이언트로부터 캐싱된 cacheCount 인입
    @Override
    public Page<BookSearchResponseDto.Response> findBooksBySimpleCategory(InquiryCategory category, String inquiryWord, Pageable pageable,Long cachedCount) {
        JPQLQuery<BookSearchResponseDto.Response> query=jpaQueryFactory.select(
                    new QBookSearchResponseDto_Response(bookEntity.bookCode,bookEntity.bookName,bookEntity.bookAuthor,bookEntity.pubDt,bookEntity.bookState,bookEntity.bookImage)
                )
                .from(bookEntity)
                .where(getSearchCategory(category,inquiryWord))
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
        ;
        
        //데이터 조회
        List<BookSearchResponseDto.Response> content = query.fetch();

        //총 갯수(cacheCount 인입 여부에 따른 count조회 수행)
        long totalCount = cachedCount != null ? cachedCount : query.fetchCount();

        return new PageImpl<>(content,pageable,totalCount);
    }
 	...
 }

 

 

React

Books.tsx

function Books(){
	...
    const [totalCount,setTotalCount] = useState(-1);

    //현재 페이지
    const currentPage = .. ;
    //페이지 당 개수
    const sizePerPage = .. ; 

    const {data,isLoading} = useQuery(
        ["inquiryBooksFetch",cateogry+"/"+inquiryWord+"/"+currentPage+"/"+sizePerPage], //쿼리키 , 쿼리키로 구분해서 data fetching
        ()=>inquiryBooksFetch(cateogry,inquiryWord,currentPage,sizePerPage,totalCount), //totalCount도 함께 요청
        {
            onSuccess(data) {
                setTotalCount(data.data.totalCount); //응답받은 totalCount 저장
            },
            staleTime: 6000 *10 
        }
    );
    
    ..
    return (
        <>
            <h1>도서 조회 결과</h1>
            <div>
                {
                    isLoading
                    ? 
                        <p>isLoading</p>
                    :
                    data?.data?.bookList.map((book:IInquriyBooksReponse) =>{
                        return (
                            <div key={book.bookNo}>
                                ...
                            </div>
                        )
                    })
                }
            </div>
            <Pagination totalCount={totalCount} sizePerPage={sizePerPage} currentPage={currentPage} />
        </>
    );
    
 }

 

 

api.ts

//도서 검색
export const inquiryBooksFetch = async (category:string, inquiryWord:string,currentPage:number,size:number,totalCount:number)=>{
    const inquriyBooksUrl = totalCount>-1
    ?  //초기값이 아닌 경우(=첫 조회가 아닌 경우)
    	`/book/inquiry/${category}/${inquiryWord}?page=${currentPage}&size=${size}&cachedCount=${totalCount}` 
    : //초기값인 경우(=첫 조회인 경우)
    	`/book/inquiry/${category}/${inquiryWord}?page=${currentPage}&size=${size}`;
    
    return await PublicAPI.get(inquriyBooksUrl)
            .then(response=>response.data);
}

 

첫 페이지 조회)

첫 페이지 조회 시엔 totalCount의 State값인 -1 로 api.ts 파일 내 fetch메소드로 들어오게 된다.

이 경우엔 서버요청 시 cachedCount값이 빠진 채 요청된다.

첫 페이지 요청에 대한 결과값으로 totalCount값을 전달받고 해당 값을 onSuccess 구문 내에서 state값에 세팅하게된다.

 

이후 페이지 조회)

그 이후 요청에선 0 이상의 totalCount를 받았을 것이니 분기처리하여 존재하는 cachedCount값을 포함시켜 요청하게 된다. 서버에선 cachedCount값이 들어왔으니 count조회 쿼리를 실행시키지 않는다.


실행시간

첫 페이지 이후 조회(10,000건 대상) 개선 전 개선 후
소요 시간 43ms 3ms

 

 

참고)

https://jojoldu.tistory.com/531?category=637935

 

3-2. 페이징 성능 개선하기 - 첫 페이지 조회 결과 cache 하기

모든 코드는 Github에 있습니다. 지난 시간에 이어 count와 관련된 2번째 개선 방법은 첫 번째 쿼리의 결과를 Cache하기 인데요. 방법은 간단합니다. 처음 검색시 조회된 count 결과를 응답결과로 내려

jojoldu.tistory.com

 

'SpringBoot' 카테고리의 다른 글

Spring Annotation 활용기  (0) 2024.05.31
@OneToOne  (2) 2024.04.22
OpenFeign 사용 시 헤더 값 넘기기  (0) 2024.03.27
DB-> Entity, Entity->DB 자동 변환 (@Convert)  (0) 2023.05.14
Lombok 어노테이션  (0) 2022.12.06