Query DSL 성능 개선 및 fetch join 고찰

이진우·2024년 8월 31일
0

스프링 학습

목록 보기
39/41
post-thumbnail

지난 포스팅 요약

지난 포스팅에서 주로 여러 실험 과정을 위주로 소개하였기 때문에
이번에는 조금 더 요약 및 보완을 하여서 포스팅을 작성한다.

우리 서비스에서..

이와 같이 동적으로 쿼리 DSL 을 활용하여

태그 기반으로 게시글을 검색할 수 있는데

fetch Join 을 통해서 쿼리를 하나라도 더 줄이려고 노력한 상태였다.

```java
@Override
    public Page<EmployeePost> showEmployeePostListWithPage(final EmployeePostSearch employeePostSearch,final Pageable pageable){


       //workFieldChildTag 를 전부 포함하고 있는 EmployeePostID 리스트 추출 
        List<Long> employeePostTmpList = queryFactory
                .select(employeePostWorkFieldChildTag.employeePost.id)
                .from(employeePostWorkFieldChildTag)
                .where(employeePostWorkFieldChildTag.workFieldChildTag.id.in(employeePostSearch.getWorkFieldChildTagId()))
                .groupBy(employeePostWorkFieldChildTag.employeePost.id)
                .having(employeePostWorkFieldChildTag.workFieldChildTag.id.count().eq((long) employeePostSearch.getWorkFieldChildTagId().size()))
                .fetch();

               //where 조건을 통해서 동적으로 결합이 가능 
                List<EmployeePost> content = queryFactory
                .selectFrom(employeePost)
                .leftJoin(employeePost.basicPostContent.workFieldTag).fetchJoin()
                .join(employeePost.basicPostContent.member).fetchJoin()
                .where(checkChildIdByEmployeePostId(employeePostTmpList,employeePostSearch.getWorkFieldChildTagId())
                        ,greaterThanMinCareer(employeePostSearch.getMinCareer()),lowerThanMaxCareer(employeePostSearch.getMaxCareer())
                        ,workFieldIdEqWithEmployeePostTmpList(employeePostSearch.getWorkFieldId()))
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .orderBy(employeePostSort(pageable))
                .fetch();

        Long count = queryFactory
                .select(employeePost.count())
                .from(employeePost)
                .where(checkChildIdByEmployeePostId(employeePostTmpList,employeePostSearch.getWorkFieldChildTagId())
                        ,greaterThanMinCareer(employeePostSearch.getMinCareer()),lowerThanMaxCareer(employeePostSearch.getMaxCareer())
                        ,workFieldIdEqWithEmployeePostTmpList(employeePostSearch.getWorkFieldId()))
                .fetchOne();


        return new PageImpl<>(content,pageable,count);


    
   }

따라서 EmployeePost 를 조회할 때 member 와 작업 태그를 fetch join 하여 select 절에 member 에 대한 정보를 함께 영속성 컨텍스트에 저장하여 나중에 member 에 대해 DB를 조회하지 않아도 되는 이점을 가질 수 있다.

만약 fetch join 을 적용하지 않는다면 검색 결과에 member 에 대한 정보인 memberNickName 등을 조회할 경우 그제서야 DB에 쿼리를 날리기 때문에 쿼리의 개수 측면에서는 확실히 이득이라고 생각했고 지금까지 이런 경우에는 무조건 fetch join 을 깔다 싶이 하였다.

하지만 저번 포스팅을 통해 그것이 아니라는 것을 느꼈다.

실험 방식 및 결과

30명의 유저가 1초에 태그를 검색하는데 각각의 유저가 10번씩 조회하는 상황을 가정하여 보자.

상황은 상위 작업 태그만 검색한 상태여서
검색 결과가 1000건 정도 나오는 상태였다.

그 결과를 살펴보자.

평균적인 응답 시간은 3초이고 TPS가 매우 낮은 것을 확인할 수 있다.

1명의 유저가 300번 조회하는 경우는

상황이 그나마 나은데 말이다.

그래서 이렇게 생각했다.

총 요청 횟수는 300개로 같으나

유저가 동시에 조회를 했을 때는 더 성능과 TPS 가 낮은 것으로 보아

그 원인은 어떤 한 쿼리에 집중되어 있는 것 같았다.

그 증거로 아래 사진처럼 RDS Queue Depth 에 상당히 많은 count 가 쌓여있는 것을 볼 수 있었다.

그 쿼리와 원인을 찾기 위해서

쿼리를 하나하나 따져가며 검수한 끝에

아래 두 가지 조건을 모두 만족 했을 때 성능이 매우 나빠졌다.

  • member 와 join 할 것
  • select 절에 검색했을 때 불필요한 contents(내용) 이 들어 갈 것

특히 Member 와 Join 이후 페이징 처리를 할 때 그 부하가 강해지는 것을 느낄 수 있었다.

(내용이란 게시글의 내용이다. 즉 검색했을 떄는 불필요하다. )

Member 에 대한 fetch join 을 제거하자.

RDS 의 Queue Depth 의 설명을 보면 아래와 같이 나와있다.

디스크 엑세스를 위해 대기중인 대기중인 미처리 IO(읽기/쓰기) 요청입니다.

따라서 fetch join 을 쓰면 무조건 selct 절에 대용량 칼럼인 contents 와 member 의 자기소개 칼럼이 들어가서 IO 에 대해 부담이 클 것 같았다.

따라서 fetch join 을 제거해보자!

결과

그래서 내린 결론

무조건 fetch join 을 쓰는 것은 옳지 않고 상황에 따라 적합하게 진행하여야 겠다는 생각을 가졌고

확실히 select 절에 대용량 칼럼이 들어갈 때, 칼럼의 개수 및 사이즈가 큰 엔티티와 조인하게 될 때

DISK Queue Depth 에 따라 성능이 매우 나빠질 수 있다.

페이징 처리한 이후 JOIN 을 해보자

그렇다면 이렇게 해보자!

내가 한 쿼리에서 성능이 나빴던 이유는

검색 결과가 from 을 통해서 대용량 칼럼과 여러 칼럼을 가지고 있는 member 와 join 을 하고 where 을 통해서 1000건의 데이터를 select 한 이후에 limit order by 등등을 수행하기 때문이라고 생각했다.

따라서 아래와 같이

먼저 조건에 만족하는 페이징 처리가 완료된 employeePostID 리스트를

where in 쿼리 조건절로 가지고 있게 하여 where 조건절 이후의 결과를
페이징 개수만큼의 개수로 줄이고,
(기본적으로 sql 의 실행 순서는 from -> where -> join 이지만 Optimizer 의 판단하에 JOIN 을 where 보다 먼저 수행하여 JOIN 되는 개수를 where 절만큼 줄일 수 있습니다.)

member 와 join 을 하지만,
대용량 칼럼의 값은 가지고 오지 않게 DTO를 통해 select 에 선별적으로 가지고 오며,

추후 batch fetch size 를 위해 TagList 를 가진 연관관계까지 포함할 수 있게 했다.

    @Override
    public Page<EmployeeSearchResponseDto> testShowEmployeePostListWithPage(final EmployeePostSearch employeePostSearch,final Pageable pageable){

//child Tag 를 포함하는 emplyoeePost 를 가져옴
        List<Long> employeePostTmpList = queryFactory
                .select(employeePostWorkFieldChildTag.employeePost.id)
                .from(employeePostWorkFieldChildTag)
              .where(employeePostWorkFieldChildTag.workFieldChildTag.id.in(employeePostSearch.getWorkFieldChildTagId()))
                .groupBy(employeePostWorkFieldChildTag.employeePost.id)
                .having(employeePostWorkFieldChildTag.workFieldChildTag.id.count().eq((long) employeePostSearch.getWorkFieldChildTagId().size()))
                .fetch();

//where 절을 만족하는 employeePost 를 가져오되 페이징 처리까지 완료한다. 
        List<Long> employeePostTmpTmpList = queryFactory
                .select(employeePost.id)
                .from(employeePost)
                 .where(checkChildIdByEmployeePostId(employeePostTmpList,employeePostSearch.getWorkFieldChildTagId())
                        ,greaterThanMinCareer(employeePostSearch.getMinCareer()),lowerThanMaxCareer(employeePostSearch.getMaxCareer())
                        ,workFieldIdEqWithEmployeePostTmpList(employeePostSearch.getWorkFieldId()))
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .orderBy(employeePostSort(pageable),employeePost.id.desc())
                .fetch();


                List<EmployeeSearchResponseDto> content2 = queryFactory
                .selectFrom(employeePost)
                .leftJoin(employeePost.employeePostWorkFieldChildTagList
                ,employeePostWorkFieldChildTag)
                        .where(employeePost.id.in(employeePostTmpTmpList))
                .transform(groupBy(employeePost.id).list(Projections.constructor(EmployeeSearchResponseDto.class,
                        employeePost.id,employeePost.basicPostContent.title,
                        employeePost.basicPostContent.workFieldTag
                        ,employeePost.basicPostContent.member.name
                        ,employeePost.basicPostContent.accessUrl
                        ,employeePost.basicPostContent.member.sex
                        ,employeePost.basicPostContent.member.birthDay
                        ,list(Projections.constructor(
                                EmployeePostWorkFieldChildTagSearchResponseDto.class,
                                employeePostWorkFieldChildTag.id,employeePostWorkFieldChildTag.workFieldChildTag.name))
                )));



        Long count = queryFactory
                .select(employeePost.count())
                .from(employeePost)
                .where(checkChildIdByEmployeePostId(employeePostTmpList,employeePostSearch.getWorkFieldChildTagId())
                        ,greaterThanMinCareer(employeePostSearch.getMinCareer()),lowerThanMaxCareer(employeePostSearch.getMaxCareer())
                        ,workFieldIdEqWithEmployeePostTmpList(employeePostSearch.getWorkFieldId()))
                .fetchOne();


        return new PageImpl<>(orderByAccordingToIndex(content2,employeePostTmpTmpList),pageable,count);

    }
    
     public  List<EmployeeSearchResponseDto> orderByAccordingToIndex(List<EmployeeSearchResponseDto> employeeSearchResponseDtoList,
            List<Long> indexList) {

        HashMap<Long, EmployeeSearchResponseDto> hashMap = new HashMap<>(employeeSearchResponseDtoList.size());
        employeeSearchResponseDtoList.forEach(employeeSearchResponseDto -> hashMap.put(employeeSearchResponseDto.getEmployeePostId(), employeeSearchResponseDto));

        List<EmployeeSearchResponseDto> output = new ArrayList<>(employeeSearchResponseDtoList.size());

        for (Long index : indexList) {
            output.add(hashMap.get(index));
        }


        return output;

    }

위 쿼리를 수행하면

개선 쿼리 성능 결과

TPS 는 초당 114 로 이전 보다 14배 향상 시켰으며

응답 반환시간 역시 175 ms 로 이전 20배 향상 시켰다.

profile
기록을 통해 실력을 쌓아가자

0개의 댓글