Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BE] feat: 회원이 등록한 리뷰 그룹들을 조회하는 기능 구현 #1098

Open
wants to merge 15 commits into
base: develop
Choose a base branch
from

Conversation

Kimprodp
Copy link
Contributor

@Kimprodp Kimprodp commented Feb 13, 2025


🚀 어떤 기능을 구현했나요 ?

  • 회원이 등록한 리뷰 그룹을 조회하는 기능을 구현했습니다.

🔥 어떻게 해결했나요 ?

  • 응답하는 리뷰 목록에는 다음과 같은 정보가 포함됩니다.
    1. 전달된 memberId로 등록된 ReivewGroup의 정보
    1. 각 ReviewGroup에 등록된 Review의 수

구현을 위해서는 3가지 방법을 생각했습니다.

  1. memberId에 해당하는 ReviewGroup을 조회, 이후 반환된 List를 반복하며 해당 그룹에 등록된 Review 개수 조회
  2. memberId에 해당하는 ReviewGroup을 조회, 이후 memberId에 해당하는 Review 조회 -> 반환된 리뷰 그룹 List에서 해당하는 리뷰 개수 카운팅
  3. memberId에 해당하는 ReviewGroup과 해당 그룹에 등록된 리뷰의 수 같이 조회

결론적으로 3번 방법으로 구현했고, 이유는 다음과 같습니다.

  • 1번 방법은 조회된 ReviewGroup 마다 등록된 Review를 조회하기 때문에 DB에 계속 접근해야 하여 비효율적입니다. O(n2)
  • 2번 방법은 결론적으로는 쿼리를 두 번 실행하나, 코드 내부에서 연산이 많이 일어나게 됩니다.
  • 3번 방법은 한 번의 쿼리와 로직이 단순해진다는 장점이 있습니다. 다만, 이를 위해서는 ReviewGroupRepository의 해당 조회 메서드의 반환값이 리뷰 그룹 정보와 리뷰 수가 함께 존재하는 dto가 되어야 합니다. 지금까지 Repository에서 dto 형식에 맞게 조회하여 바로 반환하는 방식의 구현을 하지 않았기 때문에 고민이 되었으나, 결과적으로 위 두 방법에 비해서 효율과 가독성 및 유지보수 측면에서 상당히 큰 장점이 있다고 판단하여 선택했습니다.

추가로, ReviewGroup의 페이지네이션과 정렬을 정확하게 하기 위해서는 기준이 되는 생성일자(CreatedAt) 가 필요합니다. 엔티티에 해당 컬럼이 존재하지 않기 때문에 추가했습니다.

📝 어떤 부분에 집중해서 리뷰해야 할까요?

  • 선택한 구현 방식이 괜찮은지, 더 나은 방법은 없을지 고민해주세요.
  • 일단 시간관계상 구현 방식만 확인하고, 의견 나눈 후 세부 테스트 이어서 작성할게요!

📚 참고 자료, 할 말

Copy link

github-actions bot commented Feb 13, 2025

Test Results

173 tests  +7   170 ✅ +7   5s ⏱️ ±0s
 63 suites +2     3 💤 ±0 
 63 files   +2     0 ❌ ±0 

Results for commit 10ac07c. ± Comparison against base commit 44f413b.

This pull request removes 7 and adds 14 tests. Note that renamed tests count towards both.
reviewme.review.service.PageSizeTest ‑ [1] size=0
reviewme.review.service.PageSizeTest ‑ [2] size=-1
reviewme.review.service.PageSizeTest ‑ [3] size=51
reviewme.review.service.PageSizeTest ‑ null이_들어오면_기본값으로_설정한다()
reviewme.review.service.PageSizeTest ‑ 유효한_값이_들어오면_그_값을_설정한다()
reviewme.reviewgroup.service.ReviewGroupLookupServiceTest ‑ 리뷰_요청_코드로_리뷰_그룹을_조회한다()
reviewme.reviewgroup.service.ReviewGroupLookupServiceTest ‑ 리뷰_요청_코드에_대한_리뷰_그룹이_존재하지_않을_경우_예외가_발생한다()
reviewme.reviewgroup.repository.ReviewGroupRepositoryTest$FindByMemberIdWithLimit ‑ lastReviewGroupId_보다_이후에_등록된_리뷰_그룹이_없으면_빈_리스트를_반환한다()
reviewme.reviewgroup.repository.ReviewGroupRepositoryTest$FindByMemberIdWithLimit ‑ lastReviewGroupId가_주어지지_않을_경우_가장_최근의_리뷰_그룹부터_반환한다()
reviewme.reviewgroup.repository.ReviewGroupRepositoryTest$FindByMemberIdWithLimit ‑ lastReviewGroupId가_주어질_경우_해당ID_이후의_가장_최근의_리뷰_그룹부터_반환한다()
reviewme.reviewgroup.repository.ReviewGroupRepositoryTest$FindByMemberIdWithLimit ‑ 리뷰_그룹에_등록된_리뷰_개수를_함께_응답한다()
reviewme.reviewgroup.repository.ReviewGroupRepositoryTest$FindByMemberIdWithLimit ‑ 페이징_크기보다_적은_수의_리뷰_그룹이_등록되었으면_등록된_크기만큼만_반환한다()
reviewme.reviewgroup.repository.ReviewGroupRepositoryTest$FindByMemberIdWithLimit ‑ 페이징_크기보다_큰_수의_리뷰_그룹이_등록되었으면_페이징_크기만큼만_반환한다()
reviewme.reviewgroup.service.ReviewGroupLookupServiceTest ‑ 리뷰_요약_조회_시_리뷰_요청_코드로_리뷰_그룹을_조회한다()
reviewme.reviewgroup.service.ReviewGroupLookupServiceTest ‑ 리뷰_요약_조회_시_리뷰_요청_코드에_대한_리뷰_그룹이_존재하지_않을_경우_예외가_발생한다()
reviewme.reviewgroup.service.ReviewGroupLookupServiceTest ‑ 회원에게_등록된_리뷰_그룹을_페이지네이션을_적용하여_반환한다()
reviewme.util.PageSizeTest ‑ [1] size=0
…

♻️ This comment has been updated with latest results.

Copy link
Contributor

@nayonsoso nayonsoso left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

기능상으로 문제가 있을 것 같아서 RC 합니당~

Comment on lines +53 to +54
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사소한 부분이지만 정렬 순서를 통일해봐요!
Column 다 나오고 Embedded 오는게 좋겠네요~

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 엔티티 필드의 정렬 순서가 어노테이션 기준이 아니라, 사용빈도나 중요도에 따라 논리적인 기준이 되어야 한다고 생각해요. 따라서 생성일은 엔티티의 본질적인 필드가 아니므로 맨 마지막에 배치하는 것이 맞다고 생각해요.

다른 필드들을 봐도 현재 어노테이션 별로 정렬되어 있진 않아요. (Column과 JoinColumn 혼재)
산초는 어노테이션 기준으로 정렬하는 것을 선호하는 이유가 있을까요?

Comment on lines 1 to 3
-- 리뷰 그룹에 created_at을 추가합니다.

ALTER TABLE review_group ADD COLUMN created_at TIMESTAMP(6) NOT NULL;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

review 의 created_at 이 DATETIME(6)를 쓰고있으니 통일해봐요~

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DATETIME(6)은 지금은 사용되지 않는 (구) review 테이블입니다!
지금 사용하는 테이블은 new_reivew이고(현 시점에는 네이밍이 review로 변경됨), TIMESTAMP(6)으로 등록되어 있어요.

@@ -1,4 +1,4 @@
package reviewme.review.service;
package reviewme.util;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

패키지 변경 좋네요 ㅎㅎ

Comment on lines 53 to 57
// @LoginMemberSession LoginMember loginMember
) {
// TODO : 머지 전, 리졸버 PR 머지되면 리베이스 후 삭제 예정
long memberId = 1L;
ReviewGroupPageResponse response = reviewGroupLookupService.getMyReviewGroups(lastReviewGroupId, size, memberId);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

의도가 명확하게 todo로 표시해되어서 잘 이해할 수 있었어요😊

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

리베이스 후 변경 사항 적용했어요~

Comment on lines 27 to 31
AND (
(:lastReviewGroupId IS NULL AND :lastCreatedAt IS NULL)
OR (rg.createdAt < :lastCreatedAt)
OR (rg.createdAt = :lastCreatedAt AND rg.id < :lastReviewGroupId)
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

혹시 이 부분에서 왜 lastCreatedAt 이 사용되는지 설명해줄 수 있나요?
제가 이해가 부족하네요😓

Copy link
Contributor Author

@Kimprodp Kimprodp Feb 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지금 응답(노출)에 대한 정렬 기준은 생성일(리뷰 작성일) 기준으로 하고 있는데요,
동시성 상황에서는 더 높은 id가 더 최신의 생성일을 가진다고 보장할 수 없기 때문에 응답을 id 기준으로만 했을 때 의도한대로 동작하지 않을 수 있어요.

id가 생성일을 보장하지 않는 이유

  • 생성일은 엔티티가 생성될 때, 생성자에서 설정되고,
  • id는 트랜잭션이 커밋되고 영속화 될 때 지정되니
  • 여러 요청이 멀티스레 상황에서는 처리하는 순서에 따라 생성일이 뒤바뀔 수 있어요.

따라서 lastCreatedAt도 함께 전달해서 기준을 강화하려는 목적이에요.
하지만 이렇게 사용할 경우, 프론트에서 lastId 뿐만 아니라 lastCreatedAt 까지 같이 전달받아야 해요. (순전히 lastId만 받으면 이전에 응답한 id가 응답될 경우가 존재해요.)


쓰다보니 프론트에서 lastCreatedAt을 요청에 포함해달라고 하는 것도 지금 상황에서는 힘들 것 같고, 다른 방법으로 해결을 할 수 있을 것 같아서, 이슈 정리하고 PR 요청할게요.

해당 조회 로직에서는 lastId만 사용하는 것으로 변경하겠습니다!

Comment on lines 5 to +7
public record ReviewGroupPageElementResponse(

long reviewGroupId,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이게 있어야 사용자가 클릭했을 때 정보가 전달될 수 있겠네요👍

Comment on lines 46 to 41
if (!isLastPage) {
elements.subList(0, elements.size());
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

elements.subList(0, pageSize.size()); 여야 되지 않을까요?
그리고 elements 에 하나 잘린게 재할당되어야 합니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 큰일날뻔 했네요! 👍👍

@Kimprodp Kimprodp force-pushed the be/feat/1097-member-groups-lookup branch from 55be95f to 805e8a8 Compare February 14, 2025 08:46
Copy link
Contributor Author

@Kimprodp Kimprodp left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

리반완!

Comment on lines 53 to 57
// @LoginMemberSession LoginMember loginMember
) {
// TODO : 머지 전, 리졸버 PR 머지되면 리베이스 후 삭제 예정
long memberId = 1L;
ReviewGroupPageResponse response = reviewGroupLookupService.getMyReviewGroups(lastReviewGroupId, size, memberId);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

리베이스 후 변경 사항 적용했어요~

Comment on lines +53 to +54
@Column(name = "created_at", nullable = false)
private LocalDateTime createdAt;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 엔티티 필드의 정렬 순서가 어노테이션 기준이 아니라, 사용빈도나 중요도에 따라 논리적인 기준이 되어야 한다고 생각해요. 따라서 생성일은 엔티티의 본질적인 필드가 아니므로 맨 마지막에 배치하는 것이 맞다고 생각해요.

다른 필드들을 봐도 현재 어노테이션 별로 정렬되어 있진 않아요. (Column과 JoinColumn 혼재)
산초는 어노테이션 기준으로 정렬하는 것을 선호하는 이유가 있을까요?

Comment on lines 46 to 41
if (!isLastPage) {
elements.subList(0, elements.size());
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 큰일날뻔 했네요! 👍👍

Comment on lines 1 to 3
-- 리뷰 그룹에 created_at을 추가합니다.

ALTER TABLE review_group ADD COLUMN created_at TIMESTAMP(6) NOT NULL;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DATETIME(6)은 지금은 사용되지 않는 (구) review 테이블입니다!
지금 사용하는 테이블은 new_reivew이고(현 시점에는 네이밍이 review로 변경됨), TIMESTAMP(6)으로 등록되어 있어요.

Comment on lines 27 to 31
AND (
(:lastReviewGroupId IS NULL AND :lastCreatedAt IS NULL)
OR (rg.createdAt < :lastCreatedAt)
OR (rg.createdAt = :lastCreatedAt AND rg.id < :lastReviewGroupId)
)
Copy link
Contributor Author

@Kimprodp Kimprodp Feb 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지금 응답(노출)에 대한 정렬 기준은 생성일(리뷰 작성일) 기준으로 하고 있는데요,
동시성 상황에서는 더 높은 id가 더 최신의 생성일을 가진다고 보장할 수 없기 때문에 응답을 id 기준으로만 했을 때 의도한대로 동작하지 않을 수 있어요.

id가 생성일을 보장하지 않는 이유

  • 생성일은 엔티티가 생성될 때, 생성자에서 설정되고,
  • id는 트랜잭션이 커밋되고 영속화 될 때 지정되니
  • 여러 요청이 멀티스레 상황에서는 처리하는 순서에 따라 생성일이 뒤바뀔 수 있어요.

따라서 lastCreatedAt도 함께 전달해서 기준을 강화하려는 목적이에요.
하지만 이렇게 사용할 경우, 프론트에서 lastId 뿐만 아니라 lastCreatedAt 까지 같이 전달받아야 해요. (순전히 lastId만 받으면 이전에 응답한 id가 응답될 경우가 존재해요.)


쓰다보니 프론트에서 lastCreatedAt을 요청에 포함해달라고 하는 것도 지금 상황에서는 힘들 것 같고, 다른 방법으로 해결을 할 수 있을 것 같아서, 이슈 정리하고 PR 요청할게요.

해당 조회 로직에서는 lastId만 사용하는 것으로 변경하겠습니다!

@Kimprodp
Copy link
Contributor Author

Kimprodp commented Feb 14, 2025

테스트 + 약간의 수정 적용해서 푸시했습니다~
리뷰 전에 참고 부탁합니다.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: No status
Development

Successfully merging this pull request may close these issues.

[BE] 회원이 등록한 리뷰 그룹들을 조회한다.
2 participants