프로그래밍/spring

좋아요순 같은 복잡한 정렬 기능 추가하기 QueryDSL

승민아 2024. 2. 9. 20:52

좋아요나 댓글 개수와 같이 게시글에 포함되는 정보를 이용하여 정렬하고자 할때

게시글 테이블에 개수를 나타내는 컬럼을 추가하여

댓글 삽입도하며 게시글의 좋아요수를 올리는 방식은 쉽게 구현이 가능할 것이다.

 

그러나 좋아요 개수를 정규화하여 따로 테이블을 빼놓지 않고

비정규화하여 컬럼으로두어 관리한다면 동시성 문제나 불일치 문제가 발생할 수 있다.

 

그래서 조인을 통해 게시글 좋아요 개수나 정렬등을 구현할 수 있지만

서브쿼리를 사용하고자 한다.

 

Spring Data Jpa만 알았던 나는 이제서야.

복잡한 쿼리 조회는 QueryDSL을 통해 해결이 가능하다는걸 알았다..!!

 

Poster 테이블에는 좋아요 수를 관리하는 컬럼은 없고

좋아요 테이블인 poster_like 테이블과 1:N 관계를 갖는 상황이다.

 

현재 포스터 테이블은 어떤 특정 장소(Location)에 대한 포스터를 작성하는 상황이며

장소와 포스터는 1:N 관계인 상황이다.

 

목표한 기능은 좋아요순 정렬이므로

특정 장소의 포스터를 좋아요순으로 정렬하는 기능을 목표로한다.

 

PosterService

public PageResponse<List<PosterResponse>> getLocationPosters(Long locationId, PosterPageRequest posterPageRequest) {


    PageRequest pageRequest = posterPageRequest.makePageRequest();

    String sort = posterPageRequest.getSort();
    Page<PosterResponse> posters = null;
    if (sort.equals("recent")) {
        posters = posterRepository.searchPostersByRecent(locationId, pageRequest);
    } else if (sort.equals("like")) {
        posters = posterRepository.searchPostersByLike(locationId, pageRequest);
    } else {
        posters = posterRepository.searchPostersByRecent(locationId, pageRequest);
    }

    PageResponse<List<PosterResponse>> pageResponse = new PageResponse<>(posters);
    return pageResponse;
}

쿼리파라미터로 요청한 정렬 기준에 맞춰 페이징 처리된 데이터를 응답으로 보내게 작성했다.

 

서브쿼리를 통해 좋아요순 페이징 기능을 구현한 쿼리문을 보자.

 

PosterRepositoryImpl

@Override
public Page<PosterResponse> searchPostersByLike(Long locationId, Pageable pageable) {

    QPoster poster = QPoster.poster;
    QComment comment = QComment.comment;
    QPosterLike posterLike = QPosterLike.posterLike;

    StringPath likeCount = Expressions.stringPath("like_count");

    List<PosterResponse> posters = jpaQueryFactory
            .select(Projections.constructor(PosterResponse.class,
                    poster.id,
                    poster.writer.id,
                    poster.title,
                    poster.content,
                    poster.regDate,
                    ExpressionUtils.as(
                            JPAExpressions
                                    .select(posterLike.count())
                                    .from(posterLike)
                                    .where(posterLike.poster.id.eq(poster.id)), "like_count"),
                    ExpressionUtils.as(
                            JPAExpressions
                                    .select(comment.count())
                                    .from(comment)
                                    .where(comment.poster.id.eq(poster.id)), "comment_count")
            ))
            .from(poster)
            .where(poster.location.id.eq(locationId))
            .offset(pageable.getOffset())
            .limit(pageable.getPageSize())
            .orderBy(likeCount.desc())
            .fetch();

        JPAQuery<Long> countQuery = jpaQueryFactory
            .select(poster.count())
            .from(poster)
            .where(poster.location.id.eq(locationId));

        return PageableExecutionUtils.getPage(posters, pageable, countQuery::fetchOne);
}

 

countQuery를 따로 또 추가하여 작성하는 이유는 

count 쿼리의 경우 위의 예처럼 게시글의 개수를 구하기위해 따로 조인을 탈 필요가 없는 경우가 있기에 성능 이점을 갖는다.

.getPage의 경우 count 쿼리가 생략이 가능한 경우가 다음과 같이 있는데 이때 count 쿼리를 안날릴 수 있다.

  • 보여줘야할 컨텐츠가 페이지 사이즈보다 작을때 (맨 아래 예가 있음.)

 

응답으로 게시글 정보와 게시글에 포함된 좋아요 개수와 댓글 개수를 주기를 원한다.

ExpressionUtils.as(
    JPAExpressions
        .select(posterLike.count())
        .from(posterLike)
        .where(posterLike.poster.id.eq(poster.id)), "like_count")

ExpressionUtils.as로 별칭을 지정할 수 있다.

위의 서브 쿼리를 사용하여 다음의 쿼리를 만들어낸다.

 

select p1_0.id,p1_0.member_id,p1_0.title,p1_0.content,p1_0.reg_date,(select count(p2_0.id) from poster_like p2_0 where p2_0.poster_id=p1_0.id)
from poster p1_0;

 

쿼리 결과는 다음과 같다.

서브쿼리의 결과를 해당 게시글의 좋아요 개수를 컬럼으로 붙이는것이다.

 

 

StringPath likeCount = Expressions.stringPath("like_count");

...

.from(poster)
.where(poster.location.id.eq(locationId))
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.orderBy(likeCount.desc())
  1. where : 특정 location의 poster만 필터하자.
  2. offset : 원하는 페이지 번호
  3. limit : 페이지 크기
  4. order By : 서브쿼리의 결과를 별칭으로 만든것을 StringPath를 사용하여 order by에 사용할 수 있다.

 

+ 게시글, 댓글, 좋아요 테이블을 left join을 3번하면 될까?

처음에 다음과 같이 그냥 게시글 테이블을 기준으로 left join을 3번하여 댓글, 좋아요 개수를 구하고자 했다.

 

 

출처 :&nbsp;https://thomaslarock.com/2012/04/real-world-sql-join-examples/

 

A B와 조인을하고 그 결과에 또 C를 조인을해서 원하는 결과가 나오지 않은 것이다.

 

다음과 같이 게시글과 댓글을 조인했다.

사진을 이해하려면 복잡하기에 간단히 이해하면 댓글마다 해당 게시글의 내용이 앞의 컬럼으로 존재하는 형태이다.

이때 게시글 id가 하나의 ROW로 존재했지만 조인으로 여러개의 ROW에 동일한 게시글 id가 늘어난다.

 

이 상태에서 또 게시글의 id로 좋아요 테이블과 left join 조인을 한다면 다음과 같이

하나의 좋아요가 존재하더라도 게시글id만 보고 조인이 되므로 처음에 늘어난 게시글id에 다 조인되어 의도하지 않게 동작한다.

 

=> 댓글이 2개 좋아요가 3개인 게시글이 있지만, 게시글과 댓글의 조인으로 2개의 ROW가 생기고 그것을 또 좋아요와 조인하면

2X3으로 6개의 ROW가 생기는 것이다.

 

 

ORDER BY에 6이 들어가 6번째 컬럼인 좋아요 개수로 잘 정렬하고 있다!

두번째로 나가는 count 쿼리는 전체 개수를 구하기 위한 쿼리이다.

 

현재 게시글이 4개가 있는데 다음과 같이 한 페이지의 크기를 7로 해놓으면

posters?size=7&page=1&sort=like

추가적인 count 쿼리가 안나가는걸 볼 수 있다.