[SpringBoot] 좋아요 기능 구현 및 생각
안녕하세요~ 오늘은 읽는곳곳 프로젝트에 공유 독후감을 등록했을 때 좋아요 기능도 있었으면 좋겠다! 싶어서.. 포스팅을 하게 되었습니다. 좋아요 기능을 해본 적은 있지만 숙련되지는 못해 나중에 더 좋은 방법이 있다면 업데이트하도록 하겠습니다.
요구사항 & 정리
요구사항은 아래와 같습니다.
> 한 독후감에 1개의 좋아요만 가능하다.
> 즉 좋아요 상태의 유무 관리가 필수
여기서 Review에 좋아요 카운트 칼럼을 만들어서 직접 하면 안 되냐 생각하실 분도 계시겠지만 중복으로 좋아요가 가능하고 상태 관리를 할 필요가 없다면 그러셔도 될 것 같습니다. 하지만 좋아요를 누를 때마다 DB에 접근하여 Update를 직접 해주어야 하는 것은 비효율적이라고 생각되기에 캐시나 배치 등 여러 방법을 통한 자신만의 최적화 방법을 찾으시면 좋을 것 같습니다.. 이 부분은 저도 공부가 필요합니다 ㅎ..
구현
현 요구사항은 좋아요의 상태 관리가 필수적이기 때문에 좋아요 개수와 상태를 @Transient로 설정하여 기존 엔티티에 칼럼을 추가하지 않고 Likes 테이블을 따로 만들어 관리해 주도록 하겠습니다.
@Transient // 칼럼이 만들어지지 않는다.
private Long likeCount;
@Transient
private boolean likeState;
Entity
✅ Likes Entity를 만들어주도록 합니다. 여기서 Likes라고 하는 이유는 SQL에서 like는 문법으로 사용되는 단어이기 때문에 User를 Users나 Member로 사용하는 이유와 같다고 보시면 됩니다.
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Entity
@Table(
name="likes",
uniqueConstraints={
@UniqueConstraint(
name = "likes_uk",
columnNames={"review_id", "user_id"}
)
}
)
public class Likes {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "review_id")
private Review review;
@JsonIgnoreProperties({"reviews"})
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
@CreationTimestamp
private Timestamp createAt;
}
✅ 아래의 @Table부분을 처음 보실 수도 있습니다!
@Table(
name="likes",
uniqueConstraints={
@UniqueConstraint(
name = "likes_uk",
columnNames={"review_id", "user_id"}
)
}
)
✅ @Table 어노테이션 내에서 uniqueConstraints
속성을 사용하는 것은 JPA에서 특정 엔티티의 테이블에 대해 유니크 제약 조건
을 추가하는 방법입니다. 이 구문은 likes 테이블에 대해 review_id와 user_id칼럼 조합으로 유니크 제약 조건을 생성하고 있고 같은 사용자가 동일한 리뷰에 대해 두 번 이상 좋아요를 할 수 없도록 하는 데 목적이 있습니다.
굳이 이걸 써야 하나 싶다면 비즈니스 로직을 코드 내에서 처리할 수도 있습니다. 예를 들어, 좋아요를 추가하기 전에 데이터베이스에서 해당 사용자와 리뷰에 대한 좋아요가 이미 존재하는지 확인하는 로직을 구현할 수 있습니다.
boolean existsByUserIdAndReviewId(Long userId, Long reviewId);
✅ 모든 좋아요 기능을 수행할 때 이 구문을 사용한 뒤에 하면 됩니다. 하지만 그럼 연산 횟수도 많아지고 깔끔하지 못하다고 생각되니 @Table 어노테이션을 활용하여 설정해 주도록 합시다.
LikesRepository
이후 네이티브 쿼리를 통해 좋아요 기능을 구현해 주도록 합니다.
@Modifying
@Query(value = "INSERT INTO likes(review_id, user_id) VALUES(:reviewId, :userId)", nativeQuery = true)
void mLike(@Param("userId") Long userId, @Param("reviewId") Long reviewId);
@Modifying
@Query(value = "DELETE FROM likes WHERE review_id = :reviewId AND user_id = :userId", nativeQuery = true)
void mUnLike(@Param("userId") Long userId, @Param("reviewId") Long reviewId);
여기서 왜 네이티브 쿼리로 하냐 Likes like = new Likes()
를 통해 생성자를 만들어해 주면 되지 않냐 싶지만
네이티브 쿼리 사용시 서비스 로직
@Override
public void 좋아요(Long socialId, Long review_id) {
User user = userRepository.findBySocialId(socialId).orElseThrow();
likesRepository.mLike(user.getId(), review_id);
}
JPA 사용시 서비스 로직
@Override
public void 좋아요(Long socialId, Long review_id) {
User user = userRepository.findBySocialId(socialId).orElseThrow();
Review review = reviewRepository.findById(review_id);
Likes like = new Likes(user,review);
likesRepository.save(like)
}
✅ 생성자로 따로 관리해주어야 할 뿐 아니라 좋아요를 누를 때마다 Entity를 생성해서 save 해주고 있습니다. 언듯 봐도 비효율 적이고 reviewReposity의 의존성도 추가해주어야 합니다. 그렇기 때문에 위와 같이 정리해 주도록 합니다!
✅ 이후에 유저가 독후감을 조회해 줄 때 likeCount와 likeState를 추가해서 클라이언트 측으로 반환해줘야 합니다. 그렇기 때문에 toDto를 수정하고 Count연산을 두어 클라이언트 쪽으로 보내줍시다!!
LikesRepository 추가
Long countByReviewId(@Param("reviewId") Long reviewId);
boolean existsByUserIdAndReviewId(Long userId, Long reviewId);
✅ Spring Data Jpa를 통해 카운트를 해주는 쿼리와 내가 독후감에 좋아요를 누른 상태를 반환해 주는 쿼리를 만들어줍니다.
ReviewServiewImpl
public List<ReviewDto> 유저모든독후감조회(Long socialId) {
List<Review> reviews = reviewRepository.mFindReviewsByUser(socialId);
return reviews.stream().map(review -> {
Long likeCount = likesRepository.countByReviewId(review.getId());
boolean likeState = likesRepository.existsByUserIdAndReviewId(socialId, review.getId());
return ReviewDto.toDto(review, likeCount, likeState);
}).toList();
}
✅ 마지막으로 ReviewDto에 파라미터를 추가하여 클라이언트로 전송해 주면 끝입니다.
Test & 마무리
잘 동작하는지 보기 위해 테스트 코드도 작성해 줍니다.. 좀 시간이 걸리긴 했지만 잘 되네요,,
근데 아직도 이렇게 리뷰마다 카운트연산과 상태연산을 해줘야 하는지 의문입니다. 상태의 경우는 해당하는 모든 Review의 상태를 한 번에 갖고 와서 그때그때 연산마다 상태를 체크해 주면 좋을 것 같은데... 지금은 어떻게 해야 할지 마땅히 떠오르지 않아서 나중에 더 고민해 본 뒤 로직을 수정해 보도록 하겠습니다.
오늘도 읽어주셔서 감사합니다😄
본 게시글은 메타코딩님의 수업에서 배운내용의 일부를 적용해 보았습니다.
https://www.youtube.com/c/%EB%A9%94%ED%83%80%EC%BD%94%EB%94%A9