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

[LIME-67] 리뷰 좋아요 기능 추가 #30

Merged
merged 6 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import com.programmers.lime.domains.review.api.dto.response.ReviewGetByCursorResponse;
import com.programmers.lime.domains.review.api.dto.response.ReviewGetResponse;
import com.programmers.lime.domains.review.api.dto.response.ReviewModifyResponse;
import com.programmers.lime.domains.review.application.ReviewLikeService;
import com.programmers.lime.domains.review.application.ReviewService;
import com.programmers.lime.domains.review.application.dto.ReviewGetByCursorServiceResponse;
import com.programmers.lime.domains.review.application.dto.ReviewGetServiceResponse;
Expand All @@ -39,6 +40,7 @@
public class ReviewController {

private final ReviewService reviewService;
private final ReviewLikeService reviewLikeService;

@Operation(summary = "아이템 리뷰 등록", description = "itemId, ReviewCreateRequest을 이용하여 아이템 리뷰를 등록 합니다.")
@PostMapping()
Expand Down Expand Up @@ -106,4 +108,26 @@ public ResponseEntity<ReviewGetResponse> getReview(

return ResponseEntity.ok(response);
}

@Operation(summary = "아이템 리뷰 좋아요", description = "reviewId을 이용하여 아이템 리뷰를 좋아요 합니다.")
@PostMapping("/{reviewId}/like")
public ResponseEntity<Void> likeReview(
@PathVariable final Long itemId,
@PathVariable final Long reviewId
) {
reviewLikeService.like(itemId, reviewId);

return ResponseEntity.ok().build();
}

@Operation(summary = "아이템 리뷰 좋아요 취소", description = "reviewId을 이용하여 아이템 리뷰를 좋아요 취소 합니다.")
@DeleteMapping("/{reviewId}/like")
public ResponseEntity<Void> cancelLikedReview(
@PathVariable final Long itemId,
@PathVariable final Long reviewId
) {
reviewLikeService.unlike(itemId, reviewId);

return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.programmers.lime.domains.review.application;

import org.springframework.stereotype.Service;

import com.programmers.lime.domains.review.implementation.ReviewLikeAppender;
import com.programmers.lime.domains.review.implementation.ReviewLikeRemover;
import com.programmers.lime.domains.review.implementation.ReviewLikeValidator;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class ReviewLikeService {

private final ReviewLikeAppender reviewLikeAppender;
private final ReviewLikeRemover reviewLikeRemover;
private final ReviewLikeValidator reviewLikeValidator;

public void like(
final Long memberId,
final Long reviewId
) {
reviewLikeValidator.validateReviewLike(memberId, reviewId);
reviewLikeAppender.append(memberId, reviewId);
}

public void unlike(
final Long memberId,
final Long reviewId
) {
reviewLikeValidator.validateReviewUnlike(memberId, reviewId);
reviewLikeRemover.deleteByMemberIdAndReviewId(memberId, reviewId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ public enum ErrorCode {
REVIEW_NOT_EQUAL_ITEM("REVIEW_002", "리뷰 아이디와 아이템 아이디가 일치하지 않습니다."),
REVIEW_NOT_MINE("REVIEW_003", "리뷰 작성자와 로그인한 회원아이디가 일치하지 않습니다."),
REVIEW_BAD_SORT_CONDITION("REVIEW_004", "잘못된 리뷰 정렬 조건 입니다."),
ALREADY_REVIEW_LIKED("REVIEW_005", "이미 리뷰를 좋아요 했습니다."),
NOT_REVIEW_LIKED("REVIEW_006", "좋아요를 누르지 않은 리뷰입니다."),

// Vote
VOTE_NOT_FOUND("VOTE_001", "투표를 찾을 수 없습니다."),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.programmers.lime.domains.review.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Table(name = "review_likes")
public class ReviewLike {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;

@Column(name = "member_id")
private Long memberId;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "review_id")
private Review review;

public ReviewLike(
final Long memberId,
final Review review
) {
this.memberId = memberId;
this.review = review;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
import com.programmers.lime.domains.review.model.MemberInfoWithReviewId;
import com.programmers.lime.domains.review.model.ReviewCursorIdInfo;
import com.programmers.lime.domains.review.model.ReviewCursorSummary;
import com.programmers.lime.domains.review.model.ReviewImageInfo;
import com.programmers.lime.domains.review.model.ReviewInfo;
import com.programmers.lime.domains.review.model.ReviewSortCondition;
import com.programmers.lime.domains.review.model.ReviewSummary;
import com.programmers.lime.domains.review.repository.ReviewRepository;
Expand Down Expand Up @@ -59,9 +61,14 @@ private List<ReviewCursorSummary> getReviewCursorSummaries(
.toList();

// 리뷰 아이디를 이용하여 리뷰 정보를 가져옴
Map<Long, ReviewSummary> reviewSummaryMap = reviewRepository.getReviewSummaries(reviewIds)
Map<Long, ReviewInfo> reviewInfoMap = reviewRepository.getReviewInfo(reviewIds)
.stream()
.collect(Collectors.toMap(ReviewSummary::reviewId, Function.identity()));
.collect(Collectors.toMap(ReviewInfo::reviewId, Function.identity()));

// 리뷰 아이디를 이용하여 리뷰 이미지 정보를 가져옴
Map<Long, ReviewImageInfo> reviewImageInfoMap = reviewRepository.getReviewImageInfos(reviewIds)
.stream()
.collect(Collectors.toMap(ReviewImageInfo::reviewId, Function.identity()));
Comment on lines +64 to +71
Copy link
Member

Choose a reason for hiding this comment

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

둘을 나눠서 조회해오는 이유가 궁금합니다!
db에서 정보는 공통으로 가져오고 reviewInfo와 ReviewImageInfo로 분산해도 되지 않을까 한번생각했습니다!

Copy link
Contributor Author

@Curry4182 Curry4182 Jan 29, 2024

Choose a reason for hiding this comment

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

reviews, review_images, review_likes 이 세개의 테이블을 join 해야 하는 상황 입니다.
review 기준으로 review_images, review_likes는 one to many 관계를 가집니다.

아래 글은 인프런 CTO로 근무하시고 QueryDsl을 실무에서 직접 사용하시는 이동욱님의 글입니다. 글에서 제시하는 문제 상황이 저희와 비슷해서 가져왔습니다.
https://jojoldu.tistory.com/457

위 글에서 fetch join 조건에 대해 중간 쯤에 언급 하는데 ToOne 관계의 join은 몇 개든 사용 가능 하지만 ToMany는 하나만 사용 가능하다고 주장합니다.

위와 같은 상황이 발생하는 근본적인 이유는 다중 join을 jpql로 실행할 경우 메모리에 데이터를 들고와서 join을 하는데 이 때 메모리에서는 객체가 카티전 곱으로 중복 발생합니다.
카티전 곱으로 발생하는 객체에 대해 집계 함수를 사용하면 정확한 값이 반환되지 않거나 예외가 발생하는 등의 문제가 발생했습니다.

이런 이유로 각각의 테이블을 분사하여 group by한 집계 함수 값을 가져왔습니다.

이 부분에 대해 리팩토링을 한다면 위에서 제시한 글의 핵심 주장 처럼 hibernate.default_batch_fetch_size 옵션을 활용하여, join이 아니라 where in으로 데이터를 가져오고 dto를 변환 하도록 해야하지 않을까 생각합니다.
그렇게 하려면 Review 엔티티에 리스트로 ReviewImage 엔티티와 ReviewLikes 엔티티를 추가 해야 하는 등 추가작업이 필요 합니다.

이 추가 작업을 하게 된다면 일정대로 작업이 안 되서 위와 같이 나눠서 호출 하도록 하였습니다. !! 😊😆

Comment on lines +69 to +71
Copy link
Member

Choose a reason for hiding this comment

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

p5;
map을 사용한 이유가 궁금합니다! 이미지만 불러오고, 아래처럼 작성해도 되지 않나요??

new ReviewImageInfo(reviewId, images);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

  • map을 사용한 이유는 82번 라인에 포함되어 있는 for문을 돌 때 O(N)을 만들기 위해서 저장하고 있습니다.😊
  • 말씀해주신 것 처럼 이미지를 리스트로 받고 순서대로 넣지 않는 이유는 이미 정렬된 아이디에 맞게 데이터를 넣어야 하기 때문입니다. 😊
  • 혹시 이해가 안된다면 화상으로 이야기 하는게 좋을 것 같아요 😊


// 리뷰 아이디를 이용하여 멤버 정보를 가져옴
Map<Long, MemberInfoWithReviewId> memberInfoMap = reviewRepository.getMemberInfos(reviewIds)
Expand All @@ -71,7 +78,9 @@ private List<ReviewCursorSummary> getReviewCursorSummaries(

return reviewCursorIdInfos.stream()
.map(reviewCursorIdInfo -> {
ReviewSummary reviewSummary = reviewSummaryMap.get(reviewCursorIdInfo.reviewId());
ReviewInfo reviewInfo = reviewInfoMap.get(reviewCursorIdInfo.reviewId());
ReviewImageInfo reviewImageInfo = reviewImageInfoMap.get(reviewCursorIdInfo.reviewId());
ReviewSummary reviewSummary = ReviewSummary.of(reviewInfo, reviewImageInfo.imageUrls());
MemberInfoWithReviewId memberInfoWithReviewId = memberInfoMap.get(reviewCursorIdInfo.reviewId());
MemberInfo memberInfo = memberInfoWithReviewId.memberInfo();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.programmers.lime.domains.review.implementation;

import org.springframework.stereotype.Component;

import com.programmers.lime.domains.review.domain.Review;
import com.programmers.lime.domains.review.domain.ReviewLike;
import com.programmers.lime.domains.review.repository.ReviewLikeRepository;

import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class ReviewLikeAppender {

private final ReviewLikeRepository reviewLikeRepository;

private final ReviewReader reviewReader;

public void append(
final Long memberId,
final Long reviewId
) {
Review review = reviewReader.read(reviewId);

ReviewLike reviewLike = new ReviewLike(
memberId,
review
);

reviewLikeRepository.save(reviewLike);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.programmers.lime.domains.review.implementation;

import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import com.programmers.lime.domains.review.repository.ReviewLikeRepository;

import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class ReviewLikeReader {

private final ReviewLikeRepository reviewLikeRepository;

public boolean alreadyLiked(
final Long memberId,
final Long reviewId
) {
if (memberId == null) {
return false;
}

return reviewLikeRepository.existsByMemberIdAndReviewId(memberId, reviewId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.programmers.lime.domains.review.implementation;

import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import com.programmers.lime.domains.review.domain.Review;
import com.programmers.lime.domains.review.repository.ReviewLikeRepository;

import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class ReviewLikeRemover {

private final ReviewReader reviewReader;
private final ReviewLikeRepository reviewLikeRepository;

@Transactional
public void deleteByMemberIdAndReviewId(
final Long memberId,
final Long reviewId
) {
Review review = reviewReader.read(reviewId);
reviewLikeRepository.deleteReviewLikeByMemberIdAndReview(memberId, review);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.programmers.lime.domains.review.implementation;

import org.springframework.stereotype.Component;

import com.programmers.lime.error.BusinessException;
import com.programmers.lime.error.ErrorCode;

import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class ReviewLikeValidator {

private final ReviewLikeReader reviewLikeReader;

public void validateReviewLike(
final Long memberId,
final Long reviewId
) {
if (reviewLikeReader.alreadyLiked(memberId, reviewId)) {
throw new BusinessException(ErrorCode.ALREADY_REVIEW_LIKED);
}
}

public void validateReviewUnlike(
final Long memberId,
final Long reviewId
) {
if (!reviewLikeReader.alreadyLiked(memberId, reviewId)) {
throw new BusinessException(ErrorCode.NOT_REVIEW_LIKED);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.programmers.lime.domains.review.model;

import java.util.List;

import lombok.Builder;

@Builder
public record ReviewImageInfo(
Long reviewId,
List<String> imageUrls
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.programmers.lime.domains.review.model;

import java.time.LocalDateTime;

import lombok.Builder;

@Builder
public record ReviewInfo(

Long reviewId,

int rate,

String content,

Long likeCount,

LocalDateTime createdAt,

LocalDateTime updatedAt
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,25 @@ public record ReviewSummary(

List<String> imageUrls,

Long likeCount,

LocalDateTime createdAt,

LocalDateTime updatedAt
) {

public static ReviewSummary of(
final ReviewInfo reviewInfo,
final List<String> imageUrls
) {
return ReviewSummary.builder()
.reviewId(reviewInfo.reviewId())
.rate(reviewInfo.rate())
.content(reviewInfo.content())
.imageUrls(imageUrls)
.likeCount(reviewInfo.likeCount())
.createdAt(reviewInfo.createdAt())
.updatedAt(reviewInfo.updatedAt())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.programmers.lime.domains.review.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.programmers.lime.domains.review.domain.Review;
import com.programmers.lime.domains.review.domain.ReviewLike;

public interface ReviewLikeRepository extends JpaRepository<ReviewLike, Long> {

void deleteReviewLikeByMemberIdAndReview(
final Long memberId,
final Review review
);

boolean existsByMemberIdAndReviewId(
final Long memberId,
final Long reviewId
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@

import com.programmers.lime.domains.review.model.MemberInfoWithReviewId;
import com.programmers.lime.domains.review.model.ReviewCursorIdInfo;
import com.programmers.lime.domains.review.model.ReviewImageInfo;
import com.programmers.lime.domains.review.model.ReviewInfo;
import com.programmers.lime.domains.review.model.ReviewSortCondition;
import com.programmers.lime.domains.review.model.ReviewSummary;

public interface ReviewRepositoryForCursor {
List<ReviewCursorIdInfo> findAllByCursor(
Expand All @@ -17,7 +18,9 @@ List<ReviewCursorIdInfo> findAllByCursor(

List<MemberInfoWithReviewId> getMemberInfos(List<Long> reviewIds);

List<ReviewSummary> getReviewSummaries(List<Long> reviewIds);
List<ReviewImageInfo> getReviewImageInfos(List<Long> reviewIds);

List<ReviewInfo> getReviewInfo(List<Long> reviewIds);

int getReviewCount(Long itemId);
}
Loading
Loading