Camp/정리

JPA를 활용한 join 구현과 Project Trouble Shooting

뭔가 한다 2024. 11. 24. 16:22

내일배움캠프 SPRING과정 진행중 팀프로젝트로 뉴스피드 프로젝트를 진행.

문제의 시작

우리의 사이트 컨셉은 스택오버플로우와 같은 개발 지식 공유 페이지로 정해서 진행하게 되었다.

나는 게시글 API 에 대한 파트를 담당했다.

게시글 전체 검색할 때 친구 추가한 사용자의 경우에 게시글, 댓글, 좋아요, 유저 정보 등 모든 테이블의 값들을 join해서 사용해야 했는데 jpa query에서는 join에 대한 것이 없어서 JPQL을 사용해서 쿼리를 직접 입력하여 진행하게 되었다.

 

sql 문으로 바로 작성해서 사용하기 위해 nativeQuery = true 지정을 하여 조금더 편하게 사용 할 수 있게 하였다.

nativeQuery = true

JPA에서 join을 구현할 수 있는 방법

내가 찾아본 결과 jpa에서 join 을 사용할 수 있는 방법은 총 2가지가 있었다.

1. JPQL 사용하기

내가 사용한 방법으로 @Query 를 사용해서 직접 sql 쿼리를 입력하여 조인하는 방법 이었다.

 

2. criteria API 사용하기

JPQL 을 자바 코드로 작성하도록 도와주는 빌더 클래스 API 이다.

문법 오류를 컴파일 단계에서 잡을 수 있고 동적 쿼리를 안전하게 생성 가능한 장점이 있다고 한다. 하지만 쿼리가 길어질 경우 복잡해 지는 단점이 있다.

// Criteria 쿼리 빌더
CriteriaBuider criteriaBuilder = em.getCriteriaBuilder();

// Criteria 생성, 반환 타입 지정
CriteriaQuery<Member> createQueryValue = criteriaBuilder.createQuery(Member.class);

// from 절
Root<Member> memberTable = createQueryValue.from(Member.calss);
// select절
createQueryValue.select(memberTable);

TypedQuery<Member> query = em.createQuery(createQueryValue);
List<Member> members = query.getResultList();

 

 

이런 방식으로 query 사용이 가능

 

하지만 내가 JPQL을 사용하게 된 이유는 join이 많이 들어가게 되는 검색 쿼리에서 criteria API를 사용하게 되면 코드가 너무 복잡해질 것 같았기 때문이다. 또한, 완전히 criteria API를 이해하고 작업을 들어가기엔 시간이 부족하다고 인지하여 JPQL을 통해 쿼리를 바로 작성하는 방식으로 들어가게 되었다.


Query 조건

작성을 위한 조건 정리

  • 내가 팔로우한 사람들(status = “accepted(수락)”)의 게시글만 표시한다.
    • follow : 내가 팔로우한 사람의 게시글
  • 이미 삭제된(deleted가 true일 경우) 게시글은 나타내지 않는다.
  • 제목 검색 시 검색어를 포함한 게시글만 표시한다.
  • 기간을 검색할 경우 해당 기간의 게시글만을 표시한다.
  • 정렬을 원할 경우 생성일(createdAt), 수정일(updatedAt), 좋아요 수(likes) 기준으로 정렬한다.

출력 데이터

  • id: 게시글 아이디
  • user_id: 게시글을 작성한 유저 아이디
  • user_name: 게시글을 작성한 유저 이름
  • title: 게시글 제목
  • type: 게시글 타입
  • comments: 게시글에 달린 댓글 개수
  • created_at, updated_at: 작성일, 수정일
  • like_count: 게시글에 달린 좋아요 개수
  • like.user_id: 현재 로그인된 사용자가 좋아요 표시 했는지 여부

위의 내용을 기반으로 쿼리를 작성한다.

쿼리 설명

위의 조건을 기반으로 쿼리를 완성했다.

SELECT p.id AS id,
       p.user_id AS userId,
       u.name AS userName,
       p.title AS title,
       p.type AS type,
       COUNT(c.id) AS comments,
       p.created_at AS createdAt,
       p.updated_at AS updatedAt,
       COALESCE(like_count.likes, 0) AS likeCount,
       CASE WHEN user_like.user_id IS NOT NULL THEN true ELSE false END AS userLiked
FROM post p
JOIN user u ON p.user_id = u.id
LEFT JOIN comment c ON p.id = c.post_id
LEFT JOIN (
    SELECT post_id, COUNT(*) AS likes
    FROM post_like
    GROUP BY post_id
) like_count ON p.id = like_count.post_id
LEFT JOIN (
    SELECT l.post_id, l.user_id
    FROM post_like l
    WHERE l.user_id = :userId
) user_like ON p.id = user_like.post_id
WHERE (p.user_id IN (
    SELECT f.follow_id
    FROM friend f
    WHERE f.following_id = :userId
      AND f.status = 'accepted'
)OR p.user_id IN (
      SELECT f.following_id
      FROM friend f
      WHERE f.follow_id = :userId
      AND f.status = 'accepted'
  ))
AND p.deleted = false
AND (:title IS NULL OR p.title LIKE CONCAT('%', :title, '%'))
AND p.created_at BETWEEN :start AND :end
GROUP BY p.id, p.user_id, u.name, p.title, p.type, p.created_at, p.updated_at, like_count.likes, user_like.user_id
ORDER BY :filter DESC

어마어마한 길이의 쿼리가 나왔다.

쿼리 설명

반환 데이터

SELECT p.id AS id, -- 게시글 id
         p.user_id AS userId, -- 유저 id
         u.name AS userName, -- 유저 닉네임
         p.title AS title, -- 제목
         p.type AS type, -- 게시글 타입
         COUNT(c.id) AS comments, -- 댓글 개수
         p.created_at AS createdAt, -- 생성일
         p.updated_at AS updatedAt, -- 수정일
         COALESCE(like_count.likes, 0) AS likeCount, -- 1) 좋아요 개수
         CASE WHEN user_like.user_id IS NOT NULL THEN true ELSE false END AS userLiked -- 2) 사용자의 좋아요 여부
FROM post p
  • 좋아요 개수
COALESCE(like_count.likes, 0) AS likeCount

     likes는 서브쿼리를 통해 계산을 진행한다.(하단 설명 예정)

     COALESCE를 사용하여 개수를 카운트 하고 없을 경우 0을 반환

     COALESCE(a, b)
            - a를 0부터 value를 살핀다.
            - a의 value가 null 이라면 b의 컬럼으로 대체한다.

 

  • 사용자의 좋아요 여부
CASE WHEN user_like.user_id IS NOT NULL THEN true ELSE false END AS userLiked

      현재 사용자가 좋아요를 했는지 서브쿼리를 통해 표시한다.

 

JOIN

JOIN user u ON p.user_id = u.id
LEFT JOIN comment c ON p.id = c.post_id
LEFT JOIN (
    SELECT post_id, COUNT(*) AS likes
    FROM post_like
    GROUP BY post_id
) like_count ON p.id = like_count.post_id
LEFT JOIN (
    SELECT l.post_id, l.user_id
    FROM post_like l
    WHERE l.user_id = :userId
) user_like ON p.id = user_like.post_id
  • 유저 테이블
JOIN user u ON p.user_id = u.id

 

  • 게시글 작성자 정보
LEFT JOIN comment c ON p.id = c.post_id

 

  • 게시글 댓글 수
LEFT JOIN (
      SELECT post_id, COUNT(*) AS likes
      FROM post_like
      GROUP BY post_id
) like_count ON p.id = like_count.post_id

post_like 테이블에서 post_id별로 그룹화 해서 count를 통해 likes수를 반환

      ON p.id = like_count.post_id

            post_id와 like count 서브 쿼리의 post_id를 기준으로 조인한다.

 

  • 사용자 가 좋아요 표시 했는지 확인
LEFT JOIN (
      SELECT l.post_id, l.user_id
      FROM post_like l
      WHERE l.user_id = :userId
) user_like ON p.id = user_like.post_id

post like 테이블의 사용자 확인 후 like처리를 했는지 반환

:user_id 로 받은 사용자(로그인된 사용자) 의 좋아요 정보를 조회하여 존재하였으면 데이터를 반환 아닐 경우 null을 반환 한다.

 

WHERE

WHERE (p.user_id IN (
    SELECT f.follow_id
    FROM friend f
    WHERE f.following_id = :userId
      AND f.status = 'accepted'
)OR p.user_id IN (
    SELECT f.following_id
    FROM friend f
    WHERE f.follow_id = :userId
    AND f.status = 'accepted'
))
AND p.deleted = false
AND (:title IS NULL OR p.title LIKE CONCAT('%', :title, '%'))
AND p.created_at BETWEEN :start AND :end

현재 login된 유저와 친구인 사용자가 작성한 게시글을 조회한다.

친구 테이블의 경우 following과 follow 아이디가 나누어진 상태로 accepted를 통해 서로의 친구 신청 상태를 확인하기 때문에 OR를 사용해 following, follow부분을 둘 다 받아올 수 있도록 처리 했다.

 

AND p.deleted = false

삭제된 게시글이 아닌 경우에만 조회

  • deleted = ture : 삭제한 게시글

 

AND (:title IS NULL OR p.title LIKE CONCAT('%', :title, '%'))

title검색어가 들어왔을 경우에는 게시글의 title에서 검색어가 포함된 데이터를 검색하여 반환한다.

 

AND p.created_at BETWEEN :start AND :end

기간별 검색의 경우에는 start = 1900-01-01, end = now() 로 기본값을 오기때문에 항상 사용된다.

between을 사용하여 기간 기준에 맞는 부분을 검색하여 기간 안의 데이터들만 반환하였다.

 

GROUP BY, ORDER BY

GROUP BY p.id, p.user_id, u.name, p.title, p.type, p.created_at, p.updated_at, like_count.likes, user_like.user_id
ORDER BY :filter DESC

group by의 경우 각 게시글을 id기준으로 그룹화 하여 중복 데이터를 제거하였다.

order by의 경우 filter를 통해 들어온 값으로 정렬을 하여주었다.

  • createdAt(defualt), updatedAt, likes

정리 : 어려웠던 점

쿼리를 작성하면서 이렇게까지 길어질 거라 생각을 못했기때문에 시간이 상당히 오래 걸리게 되었다. 또한 기능 구현을 진행 하면서 각 기능이 새로 생성됨에 따라 쿼리가 계속 변경 되어야 했기에 다음에는 좀 코드가 더러워지더라도 criteria API사용하는 방법을 생각해 봐야 할 것 같다.