본문 바로가기
Camp/정리

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

by 뭔가 한다 2024. 11. 24.

내일배움캠프 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사용하는 방법을 생각해 봐야 할 것 같다.

 

'Camp > 정리' 카테고리의 다른 글

[KPT 회고] 아웃소싱 프로젝트  (0) 2024.12.11
[KPT 회고] 뉴스피드 프로젝트  (0) 2024.11.25
[OOP] SOLID 원칙  (0) 2024.11.12
[TIL/JAVA] ArithmeticException  (0) 2024.10.15
1주차 프로젝트 회고록  (0) 2024.10.07