`StudyWithMe` 프로젝트를 진행하며, 내가 좀 신경 쓴 쿼리들(JPQL, Native Query 등)에 관해 포스팅하려고 한다.
(지속적으로 이 포스팅에 추가할 예정이다.)
엄청나게 어려운 쿼리들을 위주로 작성한다는 느낌보다는 내가 새롭고 기록해두면 좋겠다고 생각하는 것들을 포스팅한다.
시작!
요구사항
1. "스터디 룸 이용후기에 관한 답글"은 해당 "스터디 룸의 관리자"와 "이용후기 작성자"만 생성할 수 있다.
기본적으로, 이용후기를 작성한다는 개념 자체가 이용을 한 사람이 달아야 하며,
이용후기에 관한 답글도 스터디 룸 관리자 혹은 이용후기 작성자만 다는 것이 맞다.
그래서, 비즈니스 로직에서 해당 작업을 적절히 확인을 해주어야 한다.
이때, 프론트엔드에게 스터디 룸의 ID를 받아오고 스터디 룸 이용후기 ID를 받아온 뒤,
각각에 관한 USER ID를 가져오는 방법으로 하면 비즈니스 로직은 아주 쉽게 작성할 수 있다.
그러나, 현재 위의 상황에서 스터디 윗 미 테이블의 관계는 아래와 같다.
`스터디 룸 이용후기 : 스터디 룸 <- 다 : 일`
`스터디 룸 이용후기 : 유저 <- 다 : 일`
`스터디 룸 : 유저 <- 다 : 일`
정리하자면 "스터디 룸 이용후기에 관한 답글"은 해당 "스터디 룸의 관리자"와 "이용후기 작성자만 생성"을 수행하기 위해
필요한 데이터들은 전부 다 : 일 관계이다.
이 말은 즉, 스터디 룸 이용후기와 필요로 하는 데이터가 전부 `fetch join`이 필요하지 않으며,
조인으로 인한 데이터 펌핑이 일어나지 않는다는 것을 말한다.
"그럼 굳이 그렇게 따로 조회하지 않고도 적절한 쿼리를 작성한다면 위 1번 요구사항을 해결할 수 있지 않을까?"
위 생각으로 작성한 코드는 다음과 같다.
StudyRoomCommandService
@Transactional
public CreateStudyRoomReviewReplyResponse createReviewReply(
CreateStudyRoomReviewReplyRequest request,
Long studyRoomReviewId,
UUID userId
){
StudyRoomReview studyRoomReview = validateReviewAndGetStudyRoomAndUser(studyRoomReviewId, userId);
User user = userRepository.getReferenceById(userId);
StudyRoomReviewReply studyRoomReviewReply = StudyRoomReviewReply.of(request.getReply(), studyRoomReview, user);
reviewReplyRepository.save(studyRoomReviewReply);
return CreateStudyRoomReviewReplyResponse.from(studyRoomReviewReply);
}
StudyRoomReviewRepository
@Query(value = "select srr.* " +
"from study_room_review srr " +
"inner join study_room sr on srr.study_room_id = sr.id " +
"inner join users u on sr.user_id = u.id " +
"where srr.id = :studyRoomReviewId and (srr.user_id = :userId or u.id = :userId) " +
"and srr.deleted_at is null", nativeQuery = true)
Optional<StudyRoomReview> findByStudyRoomReviewWithNativeQuery(
@Param("studyRoomReviewId") Long studyRoomReviewId,
@Param("userId") UUID userId);
`Native Query`로 작성하는 것이 좀 더 편해서 위 처럼 `Native Query`로 작성했다.
위 코드를 설명하자면 다음과 같다.
- `study_room_review`와 `study_room`을 `inner join`한다.
- `study_room`과 `users`를 `inner join` 한다.
- 그 후, `studyRoomReviewId`를 가지며 이용후기를 작성한 사람 혹은 스터디 룸의 권한을 가지고 있는 유저가
댓글을 작성할 경우 관련된 `StudyRoomReview`를 반환한다. - 그 후, 이용후기 답글 생성 로직을 수행한다.
위처럼 코드를 작성했을 경우 요구사항대로 잘 수행됨을 확인했고 불필요한 쿼리가 나가는 것을 없앨 수 있었다.
물론, 앞서 말한 것처럼 쿼리 1개의 차이기 때문에 좀 더 구현의 단순함이라는 장점으로 작업할 수 있었다.
그러나, 주어진 환경을 최대한 활용한다는 측면에서는 다소 아쉬운 부분이 있기에 위처럼 작업했다.
2. 이용후기 조회 시 이미지가 있는 이용후기만을 조회한다.
우리가 배달의 민족에서도 볼 수 있는 것처럼 이미지만 있는 리뷰를 조회하고 싶은 경우가 있다.
확실히 눈에 보여야 신뢰가 가능하기도 하고 눈을 통해 자기가 경험하기에 그럴 수도 있다.
(필자도 배민 리뷰를 볼때면 실제 음식 사진을 자주 보는 것 같다.)
아무튼, 스터디 윗 미에서도 이미지가 있는 이용후기만을 볼 수 있도록 설계를 했다.
그래서, 해당 요구사항에 맞춰 구현을 하려고 보니,
스터디 룸 이용후기를 3개씩 끊어서 `Pagination`을 하는 과정에서 문제가 생겼다.
스터디 룸 이용후기와 관련된 스터디 룸 이용후기 이미지가 있는 데이터만을 선별해야 하는데
1.
스터디 룸 이용후기와 관련된 스터디 룸 이용후기 이미지를 `fetch join`해서 가져오자니,
스터디 룸 이용후기와 스터디 룸 이용후기 이미지는 `일 : 다` 관계이므로, 데이터 펌핑으로 인해
제대로 `Pagination` 작업이 일어나지 않을 뿐더러,
2.
`fetch join`을 사용하지 않고 `Lazy Loading` + `Batch`로 가져오자니
한 페이지 내의 3개의 스터디 룸 이용후기 중에서 스터디 룸 이용후기 이미지가 없는 경우에
한 페이지 내 3개의 데이터에 빵꾸가 난다.
즉, `Pagination` 작업을 위해선 다른 방법을 사용해야 했다.
그래서 내가 선택한 방식은 아래와 같이 `EXISTS`를 사용하는 방식이다.
StudyRoomReviewQueryService
@Transactional(readOnly = true)
public PageResponse<GetStudyRoomReviewResponse> getStudyRoomReviews(
Long studyRoomId,
boolean onlyImage,
int pageNo
) {
Pageable pageable = PageRequest.of(pageNo, reviewPageSize, Sort.by("id").descending());
Page<StudyRoomReview> pagedReviews;
if (onlyImage) {
pagedReviews = reviewRepository.findPagedReviewWithOnlyImageAndUserByStudyRoomId(studyRoomId, pageable);
} else {
pagedReviews = reviewRepository.findPagedReviewWithUserByStudyRoomId(studyRoomId, pageable);
}
// 추후, 본인 글 수정 가능 표시 추가 가능
return PageResponse.of(
pagedReviews, review -> GetStudyRoomReviewResponse.of(review, review.getReplies())
);
}
StudyRoomReviewRepository
@Query("""
select s from StudyRoomReview s join fetch s.user
where s.studyRoom.id = ?1 and
exists (select 1 from StudyRoomReviewImage sri where sri.studyRoomReview.id = s.id)
""")
Page<StudyRoomReview> findPagedReviewWithOnlyImageAndUserByStudyRoomId(Long studyRoomId, Pageable pageable);
"이미지만 보기"를 제공하는 타 서비스들이 어떻게 코드를 작성하는지는 미지수지만, 나는 `EXISTS`를 사용했다.
(물론, 엄청나게 대단한건 아니지만 Spring에서 EXISTS를 사용해 본 경험이 적어 새롭다.)
코드를 살펴보면,
특정, `studyRoomId`를 갖는 `StudyRoomReview` 중
`StudyRoomReviewImage`에 해당 `studyRoomReview.id`가 존재하는 경우
해당 `StudyRoomReview`를 가지고 온다.
위 코드로 작성했을 때 "이용후기 조회 시 이미지가 있는 이용후기만을 조회한다"는 요구사항을 충족할 수 있었다.
'프로젝트 > StudyWithMe' 카테고리의 다른 글
[StudyWithMe] 스터디 윗 미 dev 무중단 배포 (0) | 2024.12.24 |
---|---|
[StudyWithMe] 영속성 컨텍스트와 관련된 퀴즈 풀어보실 분? (0) | 2024.12.23 |
[StudyWithMe] 멀티 스레드에서 트랜잭션 작업 간 정보 불일치 문제 (0) | 2024.12.08 |
[StudyWithMe] 페이징 시 Offset 방식 대신 Cursor 방식 적용(약 111배 성능 향상) (0) | 2024.12.07 |
[StudyWithMe] 스터디 룸 UPDATE API 작성 시 했던 고민들 (0) | 2024.12.06 |