Backend/🌿Spring

[SpringBoot] 좋아요 기능 구현 및 생각

발달중인 망고 2024. 3. 27. 20:28

안녕하세요~ 오늘은 읽는곳곳 프로젝트에 공유 독후감을 등록했을 때 좋아요 기능도 있었으면 좋겠다! 싶어서.. 포스팅을 하게 되었습니다. 좋아요 기능을 해본 적은 있지만 숙련되지는 못해 나중에 더 좋은 방법이 있다면 업데이트하도록 하겠습니다.

 

요구사항 & 정리

요구사항은 아래와 같습니다.

 

> 한 독후감에 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