내일배움캠프 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 |