ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • QueryDsl 페이징 최적화 PageImpl vs PageableExecutionUtils 차이
    Spring/Study 2023. 11. 14. 01:07

    PageImpl vs PageableExecutionUtils

    return PageableExecutionUtils.getPage(result, pageable, countQuery::fetchOne);
    
    return new PageImpl<>(result, pageable, count);
    

    둘의 차이는 무엇일까?

    상황 예시

    24개의 content가 존재한다고 가정해보자
    PageSize = 10 이라고 하면 페이지는 1,2,3 페이지가 존재하게 된다.

    1, 2 페이지에서는 10개의 content가 채워져서 페이지 result가 반환되기 때문에
    PageImpl, PageableExecutionUtils 둘 다 count 쿼리가 발생한다.

    그런데 3페이지 마지막 페이지에서는 차이가 발생한다.

    3페이지에서는 4개의 content가 채워지기 때문에 2*10 + 4 계산이 가능하다.
    따라서 현재 페이지수와 현재 페이지의 content 수를 알면 값 계산이 가능하기 때문에 count 쿼리가 발생하지 않는다.

    그렇다면?

    PageImpl은 항상 count 쿼리로 전체 내용물 개수를 조회하기 때문에 대량 연산에 불리하다.

    실제 쿼리를 실행해보자

    추가 쿼리가 발생해서 3번 요청이 발생

    select
            answer.member.nickname as writer,
            answer.content as content,
            answer.auditEntity.createdAt as createdAt 
        from
            Answer answer 
        where
            answer.question.id = ?1 */ select
                m1_0.nickname,
                a1_0.content,
                a1_0.created_at 
            from
                answer a1_0 
            join
                member m1_0 
                    on m1_0.id=a1_0.member_id 
            where
                a1_0.question_id=? 
            offset
                ? rows 
            fetch
                first ? rows only
    
    select
            answer.member.nickname as writer,
            answer.content as content,
            answer.auditEntity.createdAt as createdAt 
        from
            Answer answer 
        where
            answer.question.id = ?1 */ select
                m1_0.nickname,
                a1_0.content,
                a1_0.created_at 
            from
                answer a1_0 
            join
                member m1_0 
                    on m1_0.id=a1_0.member_id 
            where
                a1_0.question_id=? 
            offset
                ? rows 
            fetch
                first ? rows only
                
    select
            count(answer) 
        from
            Answer answer 
        where
            answer.question.id = ?1 */ select
                count(a1_0.id) 
            from
                answer a1_0 
            where
                a1_0.question_id=?
    

    추가요청을 생략해서 2번 요청을 진행

    select
            answer.member.nickname as writer,
            answer.content as content,
            answer.auditEntity.createdAt as createdAt 
        from
            Answer answer 
        where
            answer.question.id = ?1 */ select
                m1_0.nickname,
                a1_0.content,
                a1_0.created_at 
            from
                answer a1_0 
            join
                member m1_0 
                    on m1_0.id=a1_0.member_id 
            where
                a1_0.question_id=? 
            offset
                ? rows 
            fetch
                first ? rows only
    
    select
            answer.member.nickname as writer,
            answer.content as content,
            answer.auditEntity.createdAt as createdAt 
        from
            Answer answer 
        where
            answer.question.id = ?1 */ select
                m1_0.nickname,
                a1_0.content,
                a1_0.created_at 
            from
                answer a1_0 
            join
                member m1_0 
                    on m1_0.id=a1_0.member_id 
            where
                a1_0.question_id=? 
            offset
                ? rows 
            fetch
                first ? rows only
    

    내부 동작

    public PageImpl(List<T> content, Pageable pageable, long total) {  
      
    	super(content, pageable);  
    	  
    	this.total = pageable.toOptional().filter(it -> !content.isEmpty())//  
    				.filter(it -> it.getOffset() + it.getPageSize() > total)//  
    				.map(it -> it.getOffset() + content.size())//  
    				.orElse(total);  
    }
    
    public static <T> Page<T> getPage(List<T> content, Pageable pageable, LongSupplier totalSupplier) {  
      
    	Assert.notNull(content, "Content must not be null");  
    	Assert.notNull(pageable, "Pageable must not be null");  
    	Assert.notNull(totalSupplier, "TotalSupplier must not be null");  
    	  
    	if (pageable.isUnpaged() || pageable.getOffset() == 0) {  
    		  
    		if (pageable.isUnpaged() || pageable.getPageSize() > content.size()) {  
    			return new PageImpl<>(content, pageable, content.size());  
    			}  
    		  
    		return new PageImpl<>(content, pageable, totalSupplier.getAsLong());  
    	}  
    	  
    	if (content.size() != 0 && pageable.getPageSize() > content.size()) {  
    		return new PageImpl<>(content, pageable, pageable.getOffset() + content.size());  
    	}  
    	  
    	return new PageImpl<>(content, pageable, totalSupplier.getAsLong());  
    }
    

    PageImpl의 경우에는 단순 long 타입의 total을
    PageableExecutionUtils의 getPage는 LongSupplier라는 인터페이스가있다.

    계산 조건을 파악해보면

    • 페이지 정보가 없거나, 첫 페이지를 가져오는 경우에는 content.size()를 total로 사용한다.
    • 페이지네이션 범위가 실제 데이터 크기를 넘어서는 경우, offset + content.size()를 total로 사용한다.
    • 그 외의 경우에는 totalSupplier 함수를 호출하여 total 값을 계산한다.

    마지막 페이지의 content가 PageSize와 같다면 추가쿼리가 수행된다.

    결론적으로 페이지의 상태에 따라 계산을 할지 말지 선택하는 차이가 발생하게 된다.

    댓글

Designed by black7375.