본문 바로가기
팀 프로젝트/플러스 프로젝트

조회 성능 향상시키기(1) PageableExecutionUtils?

by pon9 2025. 3. 21.

2025.02.06 - [팀 프로젝트/플러스 프로젝트] - 조회 성능 개선 작업 정리본

최종 플젝 때 건 아니지만.. 예전에 완전한 조회속도 개선에 실패하고 후일을 도모했던 쿼리를 이제 진짜로 향상시켜보자

우선 커서기반은 제쳐두고, 오프셋 기반 페이지네이션으로 튜닝에 도전해보자

 

 

PageableExecutionUtils - fetchCount

과거의 내가 써놓은 코드를 보는데

pageable을 이런식으로 리턴했다. 이당시엔 fetch로 불러오니 당연히 빠르겠지! 하고 사용한 것 같은데.. 

해당 방식 대로 하니 8.51초가 나왔고,

Fetchable#fetchCount() was computed in memory! See the Javadoc for AbstractJPAQuery#fetchCount for more details.

이런 경고문구를 볼 수 있었다. 찾아보니 fetchCount를 group by나 having절이 포함된 복잡한 쿼리에서 사용하면 hibernate가 데이터를 조회한 후 메모리에서 count를 하게되므로

큰 결과(?) 일 경우 퍼포먼스에 패널티가.. 성능 저하가 일어난다고 한다.

그래서 카운트 쿼리를 최적화할 수 있을 경우에만 사용한다. 이런 이유들 덕분에 Deprecated List에 올라간 지원중단된 메서드였다.

http://querydsl.com/static/querydsl/5.0.0/apidocs/deprecated-list.html

 

Deprecated List (Querydsl 5.0.0 API)

JavaScript is disabled on your browser. Deprecated Methods  Method and Description com.querydsl.sql.codegen.NamingStrategy.appendSchema(String, String) com.querydsl.jpa.impl.AbstractJPAQuery.fetchCount() com.querydsl.jpa.impl.AbstractJPAQuery.fetchResults

querydsl.com

 

그래서 fetchCount말고, count쿼리를 미리 만들어두고 

fetchOne으로 결과를 조회해보자.

경고문이 사라지고, 5.21초가 나온다.

 

그렇다면 정석적인 방법인 PageImpl로도 안 해볼 수 없다

사실 인기 검색 결과 조회라 바로 POPULAR_LIMIT(200)으로 들어가게 된다. 

고정값이라 count쿼리가 실행되지 않아서 더 빨라진 모습을 볼 수 있다.

 

pageableExecutionUtils로도 고정값을 넣을 수 있다. LongSupplier에 람다식을 넣으면 된다. () -> POPULAR_LIMIT

살짝? 향상되었다.(거기서 거기)

방법  utils + fetchCount utils + 고정값 utils + fetchOne pageImpl + 고정값
속도 8.51s 4.37s 5.21s 4.46s
비고 메모리에서 fetchCount 연산 수행, 결과가 커서 오래 걸림 별도의 count쿼리 실행하지 않음 count쿼리 실행하므로 살짝 느림 기본형, count쿼리 실행하지 않음

 

'인기 거래소 조회' 말고, 나머지 쿼리도 pageableExecutionUtils와 pageImpl을 비교해보았는데, 결과는 거기서 거기였다.

PageableExecutionUtils를 사용하면 속도가 빨라진다는데.. 효과를 못보고있다. 그럼 언제 쓰는걸까?

 

 

내부 구조

코드는 되게 별거없다. 한줄요약하면 불필요한 count쿼리를 줄여서 성능을 최적화해준다.

즉, 어떤 조건에 따라 count쿼리를 utils에서 호출하기 전 까진 count쿼리가 실행되지 않는다.

지금부터 그 조건을 알아보고 왜 내 쿼리가 해당 조건에 부합하지 않는지 알아보자.

 

1.페이징이 비활성화된 경우

페이징 없이 데이터를 그대로 반환해야 하는 경우, content의 전체 크기 content.size()를 그대로 반환한다

 

2. content.size가 pagesize보다 작은경우

첫 번째 페이지이고(page=0), 데이터가 부족(pagesize > content.size)할 경우 더 이상 데이터가 없다고 가정하고 그냥 content.size를 반환한다.

첫 번째 페이지가 아니면서, 현재 페이지에 데이터가 존재하고, 데이터가 부족할 경우(대부분의 경우 마지막 페이지)

content.size + pageable.getOffset을 더해서 "최소한 이정도 개수는 존재한다고 가정하며" total값으로 반환하게된다.

페이지네이션을 구현할 때 정확한 총 개수가 필요하지 않은 경우가 많아서, countQuery를 실행하지 않도록 하고 바로 반환하도록 하는것이다.

 

그런데 오프셋 기반 페이지네이션에서 이 방법이 정말 좋은게 맞을까? 잘 모르겠다.. 오프셋 기반이면 페이지네이션 바를 지원하겠다는 건데, 정확한 total 개수 없이면 지원 불가능하지 않나?

뭐 프론트에서 미리 마지막 페이지 번호를 기억하고 처리할 수 있으려나?

실시간으로 데이터가 insert되는 환경에서 "더보기"버튼을 제공하기에는 괜찮아보이지만, 이럴거면 커서기반을 쓰는 게 좋지않나?

 

그래서 그냥 pageImpl로 하기로했다.

 

아 그리고 거래소의 경우에는 total count가 item개수로 고정되기 때문에,

왼쪽처럼 market을 조인하던 불필요한 쿼리를 제거하고 item.count만 시행하도록 해서

4초 후반대에서 3초 초반대로 줄일 수 있었다

왼쪽 - 전(4.98s)

오른쪽 - 후(3.03s)

확실히 페이지네이션은 count쿼리때문에 시간을 잡아먹는게 크다.

 

 

회고

PageableExecutionUtils는, 첫번째 페이지이거나 마지막 페이지일때, 페이지네이션이 off일때 등 특수한 상황에서만 효과를 발휘하고, 오히려 대부분의 경우 내부 if문을 거쳐야 해서 성능이 조금이지만 더 떨어지는 모습을 보니.. 내가 잘 모르는걸수도 있지만 별론거같다.

아무튼 오늘은 지원중단된 메서드를 쓰고 있던 구식 쿼리를 바꾸고,

PageableExecutionUtils를 사용하지 않고 pageImpl에서 count쿼리를 최적화하는 방향으로 개선을 진행했다.

다음은 인덱스를 사용하여 개선해보자

 

 

1. 인기 거래소 조회: 8.51s -> 4.46s

1. PagableExecutionUtils + fetchCount로 조회되던 쿼리를, PageImpl + 고정값으로 변경: 8.51s -> 4.46s

 

2. 전체 거래소 조회: 5.79s -> 3.03s

1. PageableExecutionUtils + fetchCount로 조회되던 쿼리를, PageImpl + countQuery로 변경: 5.79s -> 4.98s

2. TotalCount에 들어가는 countQuery를 최적화: 4.98s -> 3.03s

 

3. 인기 경매장 조회: 22.28s -> 9.23s

1. PagableExecutionUtils + fetchCount로 조회되던 쿼리를, PageImpl + 고정값으로 변경: 22.28s -> 9.23s

 

4. 전체 경매장 조회: 24.48s -> 10.47s

1. PageableExecutionUtils + fetchCount로 조회되던 쿼리를, PageImpl + countQuery로 변경: 24.48s -> 10.47s