[QueryDsl] 쿼리 최적화

권성현·2023년 3월 26일
0

면접 준비

목록 보기
29/30
post-custom-banner

[QueryDsl] 쿼리 최적화

이번에 프로젝트 때 QueryDsl을 처음으로 사용해봤다.

사용해보니 QueryDsl의 장점으로는 다음과 같은 장점이 있는 것 같았다.

  1. 기존에 작성하던 문자 형태로 코드를 작성하는것이 아니라 코드로 작성하기 때문에 에러 검출이 쉽다.
  2. 동적인 쿼리 작성이 가능하다
  3. SQL과 문법이 비슷해서 QueryDsl로 짜여진 코드를 봤을 때 어떤 쿼리인지 알기 쉽다.

쿼리 최적화 예시

이번 프로젝트 때 기존에 Spring Data JPA로 데이터를 Select 해오는 코드가 있었는데,

예를 들어서 특정 Post Entity의 정보를 Select 하는 쿼리

  • 해당 Post Entity의 좋아요 개수가 몇 개인지 Likes Entity를 Select하는 쿼리
  • 해당 Post에 달린 Comment가 몇 개인지 Comment Entity를 Select 하는 쿼리
  • 해당 Post에 내가 좋아요를 눌렀는지 Exists 를 사용하여 결과값을 반환하는 쿼리

기존에 쿼리를 수행 하게 되면

  1. Post Entity 조회 + 연관된 User Entity 조회(N+1)문제

  2. 각각의 좋아요 개수 / 로그인 한 유저가 좋아요를 눌렀는지 / 게시글에 댓글의 개수를 전부다

날리기 때문에 쿼리량이 엄청 방대해졌고, 댓글이 많이 달렸을 때는 더욱이 수행속도 또한 많이 느려질 수 밖에 없었다.

이걸 수정하기 위해서 각각의 테이블을 조인하고 필요한 정보를 한번에 조회하도록 SQL을 작성했다.

그리고 나서 이 SQL을 QueryDsl로 작성하는 과정을 거쳤다.

작성한 쿼리문을 토대로 만든 QueryDsl 코드는 다음과 같다.

PostResponseDto 클래스를 생성한 뒤 해당 Dto 클래스의 필드값을 Projecsion.fields로 하여 각각 채워넣은 후 리턴하였다.

❓ 기존 로직에서 엔티티 fetch type을 lazy로 설정하여 연관된 객체를 spring data jpa를 사용하여 select 할 경우 N+1 문제가 발생하였고, 추가적으로 DTO에 넣어야 할 정보를 select 하는 로직이어서 쿼리량이 방대하고 성능적으로 문제가 발생

❗️querydsl을 사용하여 DTO에 반환해야 할 정보를 한번에 select 하여 쿼리량 감소 뿐만 아니라 쿼리 수행 속도 또한 1/5 정도로 개선

.select(Projections.fields(
                    PostResponseDto.class,
                    post.id.as("postId"),
                    QUser.user.profileImage,
                    QUser.user.userId,
                    QUser.user.nickname,
                    post.title,
                    post.content,
                    post.imgUrl,
                    ExpressionUtils.as(
                             JPAExpressions
                                      .select(likes.count())
                                      .from(likes)
                                      .where(likes.post.id.eq(post.id)),
                             "postLikeNum"
                    ),
                    ExpressionUtils.as
                             (JPAExpressions
                                      .selectFrom(likes)
                                      .where(likes.userId.eq(user.getId())
                                      .and(likes.post.id.eq(post.id)))
                                      .exists(),
                              "isLikePost"),
                    ExpressionUtils.as(
                              JPAExpressions
                                      .select(comment1.count())
                                      .from(comment1)
                                      .where(comment1.post.id.eq(post.id)),
                              "commentCount"
                    ),
                    post.category,
                    post.createdAt.as("createAt"),
                    post.modifiedAt
                    )
).from(post)
.join(QUser.user)
.on(post.user.id.eq(QUser.user.id))
.orderBy(post.createdAt.desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize() + 1)
.fetch();


기존 수행되던 쿼리(아래에 더 찍히고 있다.)

쿼리 최적화 한 후 변경된 쿼리 ( 두 줄이면 조회가 끝난다.)

쿼리량 자체 변화가 눈에 띄게 줄어들었다.

쿼리 최적화 한 후 변경된 쿼리에서 첫번째 User를 구하는 쿼리는 현재 접속한 사용자의 정보를 찾기 위해

서비스 로직에서 한번 수행되는 쿼리로 한번씩 날아가는 쿼리이므로, 사실상 쿼리 하나로 필요한 정보를 전부 가지고 온다고 볼 수 있다.

그러면 이제 수행속도를 한 번 비교해 봐야할 것 같다.

간단하게 포스트맨을 통해 수행되는 시간을 찍어서 비교해보겠다.


기존 수행 쿼리의 수행 시간은 519ms

변경 후 수행 쿼리의 수행 시간은 105ms

profile
개발일지
post-custom-banner

0개의 댓글