팀프로젝트 쿼리성능최적화(QueryDsl)-게시글

2024. 8. 31. 12:31Project

이번에 성능을 최적화 할 부분은 게시글 부분이다. 물론 이곳의 코드는 내가 짜지 않았지만 쿼리성능최적화는 내가 꼭 해보고 싶었던 부분이기에 내가 하기로 결정했다. 

지난번 처럼 사진으로 먼저 페이지를 보자

 

 

먼저 게시글 목록 즉 게시판 조회이다. 바로 어떤 쿼리가 나오는지 한번 봐보도록 하자

1. 게시글 리스트 조회 성능 개선전

Hibernate: 
    select
        p1_0.id,
        p1_0.comment_count,
        p1_0.content,
        p1_0.created_at,
        p1_0.like_count,
        p1_0.member_id,
        p1_0.title,
        p1_0.updated_at,
        p1_0.views 
    from
        posts p1_0 
    order by
        p1_0.created_at desc 
    limit
        ?, ?

Hibernate: 
    select
        count(p1_0.id) 
    from
        posts p1_0

Hibernate: 
    select
        m1_0.id,
        m1_0.created_at,
        m1_0.deleted_at,
        m1_0.email,
        m1_0.name,
        m1_0.nickname,
        m1_0.password,
        m1_0.profile_image_url,
        m1_0.role,
        m1_0.user_id 
    from
        members m1_0 
    where
        m1_0.id in (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)

 

게시글들에 대한 정보, 페이징이기에 나오는 게시글의 갯수, 그리고 작성자 이렇게 3가지 쿼리가 나온다

하지만 QueryDsl을 통해 프로젝션 한다면 어떻게 쿼리가 나올까?

Hibernate: 
    select
        p1_0.id,
        p1_0.title,
        m1_0.nickname,
        p1_0.comment_count,
        p1_0.like_count,
        p1_0.created_at,
        p1_0.updated_at,
        p1_0.views 
    from
        posts p1_0 
    join
        members m1_0 
            on m1_0.id=p1_0.member_id 
    order by
        p1_0.created_at desc 
    limit
        ?, ?
Hibernate: 
    select
        count(p1_0.id) 
    from
        posts p1_0

 

굉장히 간결하게 게시글 리스트에 필요한 정보들 즉 멤버와 게시판을 한번에 가져온다.

이제 코드로 봐보도록 해보자

@Override
    public Page<PostListResponseDto> getAllPosts(Pageable pageable, String sortBy) {
        List<PostListResponseDto> results = queryFactory
                .select(Projections.constructor(
                        PostListResponseDto.class,
                        post.id,
                        post.title,
                        member.nickname,
                        post.commentCount,
                        post.likeCount,
                        post.createdAt,
                        post.updatedAt,
                        post.views
                ))
                .from(post)
                .join(post.member, member)
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .orderBy(getOrderSpecifier(sortBy))
                .fetch();

        Long total = queryFactory
                .select(post.count())
                .from(post)
                .fetchOne();
        return new PageImpl<>(results, pageable, total);
    }

이번엔 Projections.constructor를 이용해 한번에 DTO를 가져오고 총 게시글의 갯수는 따로 쿼리를 조회해 가져오도록 했다.

이를 통해 쿼리 한번에 멤버와 게시글을 모두 필요한 필드만 가져올 수 있게 됐다.

 

다음은 게시글의 상세 정보다.

 

이곳엔 게시글과, 댓글, 좋아요, 댓글의 작성자, 게시글의 작성자를 불러와야 한다.

바로 나오는 쿼리를 봐보자 이번엔 어마어마한 쿼리가 나오니 스크롤을 많이 내려야 할것이다.

1. post 조회

Hibernate: 
    select
        p1_0.id,
        p1_0.comment_count,
        p1_0.content,
        p1_0.created_at,
        p1_0.like_count,
        p1_0.member_id,
        p1_0.title,
        p1_0.updated_at,
        p1_0.views 
    from
        posts p1_0 
    where
        p1_0.id=?
2. 게시글의 좋아요 수 count

Hibernate: 
    select
        count(l1_0.id) 
    from
        likes l1_0 
    where
        l1_0.post_id=?
3. 게시글 작성자의 정보

Hibernate: 
    select
        m1_0.id,
        m1_0.created_at,
        m1_0.deleted_at,
        m1_0.email,
        m1_0.name,
        m1_0.nickname,
        m1_0.password,
        m1_0.profile_image_url,
        m1_0.role,
        m1_0.user_id 
    from
        members m1_0 
    where
        m1_0.id=?
4. 게시글 이미지 조회

Hibernate: 
    select
        i1_0.post_id,
        i1_0.id,
        i1_0.name,
        i1_0.origin_name,
        i1_0.path 
    from
        post_images i1_0 
    where
        i1_0.post_id=?
5. 게시글 조회수 증가 update문

Hibernate: 
    update
        posts 
    set
        comment_count=?,
        content=?,
        created_at=?,
        like_count=?,
        member_id=?,
        title=?,
        updated_at=?,
        views=? 
    where
        id=?
6. 게시글 다시 조회


Hibernate: 

    select
        p1_0.id,
        p1_0.comment_count,
        p1_0.content,
        p1_0.created_at,
        p1_0.like_count,
        p1_0.member_id,
        p1_0.title,
        p1_0.updated_at,
        p1_0.views 
    from
        posts p1_0 
    where
        p1_0.id=?
7. 댓글 조회


Hibernate: 
    select
        c1_0.id,
        c1_0.content,
        c1_0.created_at,
        c1_0.member_id,
        c1_0.post_id 
    from
        comments c1_0 
    where
        c1_0.post_id=?
8. 댓글단 회원 정보 조회

Hibernate: 
    select
        m1_0.id,
        m1_0.created_at,
        m1_0.deleted_at,
        m1_0.email,
        m1_0.name,
        m1_0.nickname,
        m1_0.password,
        m1_0.profile_image_url,
        m1_0.role,
        m1_0.user_id 
    from
        members m1_0 
    where
        m1_0.id in (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)

 

조회수를 증가시키고 업데이트시킨후 게시글을 다시 불러오는 이유도 있지만 어마어마한 쿼리가 나온다.

이 마저도 배치사이즈를 기본적으로 100으로 설정해놔서 간신히 N+1을 피했다.. 물론 그마저 완벽하진 않다.

 

자 이제 이 쿼리를 최적화하면 얼마나 개선이 될까?

1. 게시글 찾기

Hibernate: 
    select
        p1_0.id,
        p1_0.comment_count,
        p1_0.content,
        p1_0.created_at,
        p1_0.like_count,
        p1_0.member_id,
        p1_0.title,
        p1_0.updated_at,
        p1_0.views 
    from
        posts p1_0 
    where
        p1_0.id=?
2. 좋아요수 조회

Hibernate: 
    select
        count(l1_0.id) 
    from
        likes l1_0 
    where
        l1_0.post_id=?
3. 조회수 업데이트

Hibernate: 
    update
        posts 
    set
        comment_count=?,
        content=?,
        created_at=?,
        like_count=?,
        member_id=?,
        title=?,
        updated_at=?,
        views=? 
    where
        id=?
4. 게시글 조회

Hibernate: 
    select
        p1_0.id,
        p1_0.title,
        m1_0.nickname,
        p1_0.created_at,
        p1_0.updated_at,
        p1_0.views,
        p1_0.content,
        i1_0.path,
        i1_0.id,
        m1_0.user_id=cast(? as char(255)) 
    from
        posts p1_0 
    left join
        members m1_0 
            on m1_0.id=p1_0.member_id 
    left join
        post_images i1_0 
            on p1_0.id=i1_0.post_id 
    where
        p1_0.id=? 
    group by
        p1_0.id,
        i1_0.id
5. 멤버 검사 로직 돌면서 멤버 조회

Hibernate: 
    select
        m1_0.id,
        m1_0.created_at,
        m1_0.deleted_at,
        m1_0.email,
        m1_0.name,
        m1_0.nickname,
        m1_0.password,
        m1_0.profile_image_url,
        m1_0.role,
        m1_0.user_id 
    from
        members m1_0 
    where
        m1_0.user_id=?
6. 댓글들 가져오기

Hibernate: 
    select
        c1_0.id,
        m1_0.nickname,
        c1_0.content,
        c1_0.created_at,
        (c1_0.member_id=cast(? as signed)) 
    from
        comments c1_0 
    join
        members m1_0 
            on m1_0.id=c1_0.member_id 
    where
        c1_0.post_id=?

나도 실무를 겪어본것도 아니고 서비스를 운영한 경험이 없어서 엄청나게 드라마틱한 수준의 차이일지는 모르겠지만 

그래도 무려 쿼리를 2개나 줄였다.

우선 조회수 업데이트후 게시글을 조회하는 쿼리에서 이미지와 작성자 정보를 한번에 가져와서 쓸모없는 쿼리를 줄였다.

그리고 댓글관련부분이 정말 잘 최적화 된거 같다.

기존에는 댓글과, 댓글을 작성한 멤버의 정보를 모두 가져와서 굉장히 비효율적인 쿼리가 나왔는데 멤버의 필요한 정보와 댓글의 필요한 정보만 쏙쏙 가져와서 내가 봐도 흡족한 맘에드는 쿼리가 나왔다.

 

이제 QueryDsl을 봐보자

@Override
    public PostDetailsResponseDto findPostDetailsById(Long postId, String userId) {
        BooleanExpression authorCheckExpression = (userId != null)
                ? post.member.userId.eq(userId)
                : post.member.userId.isNull();

        Long likesCount = queryFactory
                .select(likes.count())
                .from(likes)
                .where(likes.post.id.eq(postId))
                .fetchOne();

        List<Tuple> results = queryFactory
                .select(
                        post.id,
                        post.title,
                        post.member.nickname,
                        post.createdAt,
                        post.updatedAt,
                        post.views,
                        post.content,
                        postImg.path,
                        postImg.id,
                        authorCheckExpression
                )
                .from(post)
                .leftJoin(post.member, member)
                .leftJoin(post.images, postImg)
                .where(post.id.eq(postId))
                .groupBy(post.id, postImg.id)
                .fetch();

        if (results.isEmpty()) {
            return null;
        }

        PostDetailsResponseDto dto = null;

        List<String> imageUrls = new ArrayList<>();
        List<Long> imageIds = new ArrayList<>();

        for (Tuple result : results) {
            if (dto == null) {
                dto = new PostDetailsResponseDto(
                        result.get(post.id),
                        result.get(post.title),
                        result.get(post.member.nickname),
                        result.get(post.createdAt),
                        result.get(post.updatedAt),
                        result.get(post.views).intValue(),
                        likesCount.intValue(),
                        result.get(post.content),
                        imageUrls,
                        imageIds,
                        result.get(authorCheckExpression)
                );
            }
            imageUrls.add(result.get(postImg.path));
            imageIds.add(result.get(postImg.id));
        }

        return dto;
    }

booleanExpression은 게시물 작성자의 정보를 확인하는 부분이다.

그 후 좋아요수를 쿼리를 통해 따로 계산하고게시물의 정보를 조회하는 쿼리를 작성한후 필요한 모든 정보를 DTO로 받아오는 코드이다.

 

댓글 관련 QueryDsl도 봐보자 이 부분이 최적화가 잘된것 같아 정말 맘에 든다.

 @Override
    public List<CommentListResponseDto> getAllComments(Long postId, Long currentUserId) {
        BooleanExpression authorCheckExpression = (currentUserId != null)
                ? comment.member.id.eq(currentUserId)
                : comment.member.id.isNull();

        return queryFactory
                .select(Projections.constructor(
                        CommentListResponseDto.class,
                        comment.id,
                        comment.member.nickname,
                        comment.content,
                        comment.createdAt,
                        authorCheckExpression.as("authorCheck")
                ))
                .from(comment)
                .join(comment.member, member)
                .where(comment.post.id.eq(postId))
                .fetch();

    }

아까와 같이 BooleanExpression을 통해 작성자를 확인하고 Projections.constructor를 통해 필요한 필드만 모아서 DTO에 태워 보낸다. 아직까지 QueryDsl을 잘 다루지 못하기 때문에 더 자세한 설명을 하고 싶어도 더 못적은게 아쉽다..

아직 실력이 많이 부족해서 여기서 더 최적화 할 수 있을거 같지만 아직은 잘 모르겠다 하지만 프로젝트가 완전히 마무리 되고나서 강의를 추가로 들어서 좀더 공부해봐야겠다.