(Spring) 취향 기반 향수 추천 서비스 - 10. 향수 리뷰 게시판 (리뷰 좋아요 기능)

김준석·2023년 7월 14일
0

향수 추천 서비스

목록 보기
12/21
post-thumbnail

맨 처음 설계할 때는 좋아요, 싫어요 , 취소 세가지의 기능을 생각했습니다.
다만, 실제 어플을 사용해보면 좋아요 혹은 싫어요 버튼을 두 번 누를 경우 취소되는 방식이었습니다.
또한, 싫어요 상태에서 좋아요 버튼을 클릭하면 예외를 던지지 않고 LikeStatus 상태가 변하는 방식으로 설계를 하였습니다.

PerfumeReviewBoard.java

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity(name = "perfume_review_board")
@EntityListeners(AuditingEntityListener.class)
public class PerfumeReviewBoard {
	//생략
        
    public void updatePost(String title, Content content) {
        this.title = title;
        this.content = content;
    }

    public void increaseLikeCount() {
        this.likeCount += 1;
    }

    public void decreaseLikeCount() {
        this.likeCount -= 1;
    }

    public void increaseUnlikeCount(){
        this.unlikeCount += 1;
    }
    public void decreaseUnlikeCount(){
        this.unlikeCount -= 1;
    }

}

ReviewLike.java

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity(name = "review_like")
public class ReviewLike {
	//생략
    
    public void likePost(ReviewLike like) {
        if (like.getLikeStatus() == LikeStatus.UNLIKE) {
            this.decreaseLikeCount();
            this.increaseLikeCount();
        }
        if (like.getLikeStatus() == LikeStatus.CANCELED) {
            this.increaseLikeCount();
        }
        if (like.getLikeStatus() == LikeStatus.LIKE) {
            this.decreaseLikeCount();
        }
    }

    public void unlikePost(ReviewLike like) {
        if (like.getLikeStatus() == LikeStatus.LIKE) {
            this.decreaseLikeCount();
            this.increaseUnlikeCount();
        }
        if (like.getLikeStatus() == LikeStatus.CANCELED) {
            this.increaseUnlikeCount();
        }
        if(like.getLikeStatus() == LikeStatus.LIKE){
            this.decreaseUnlikeCount();
        }
    }
}

맨 처음에는 코드를 위와 같이 작성을 하였습니다. 코드를 작성하고 테스트코드까지 다 만든 뒤에 다시 코드를 보는데 뭔가 어색한 부분이 보였습니다.
ReviewLike 객체는 현재 상태가 Like상태인지 UnLike상태인지 Canceled된 상태인지 판단하는 책임만을 가져야 하는데, ReviewBoard의 likeCount 상태를 변경하는 역할까지 하고 있었습니다.
따라서, ReviewLike는 상태만 관리하는 역할을 하고 실질적으로 Counting해주는 기능은 분리해야겠다고 생각하였습니다.

도메인 책임 분리 리팩토링

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity(name = "perfume_review_board")
@EntityListeners(AuditingEntityListener.class)
public class PerfumeReviewBoard {
	//중략

    public void likePost(ReviewLike like) {
        if (like.getLikeStatus() == LikeStatus.UNLIKE) {
            this.decreaseLikeCount();
            this.increaseLikeCount();
        } else if (like.getLikeStatus() == LikeStatus.CANCELED) {
            this.increaseLikeCount();
        } else {
            this.decreaseLikeCount();
        }
    }

    public void unlikePost(ReviewLike like) {
        if (like.getLikeStatus() == LikeStatus.LIKE) {
            this.decreaseLikeCount();
            this.increaseUnlikeCount();
        } else if (like.getLikeStatus() == LikeStatus.CANCELED) {
            this.increaseUnlikeCount();
        } else {
            this.decreaseUnlikeCount();
        }
    }
}

PerfumeReviewBoard는 LikeStatus의 상태를 보고 Count를 할지 말 지 결정하도록 개선하였습니다.

public class ReviewLike {
	//중략
    public LikeStatus updateLike() {
        if (this.likeStatus == LikeStatus.UNLIKE) {
            return this.likeStatus = LikeStatus.LIKE;
        }
        if (this.likeStatus == LikeStatus.LIKE) {
            return this.likeStatus = LikeStatus.CANCELED;
        }
        return this.likeStatus = LikeStatus.LIKE;
    }

    public void updateUnLike() {
        if (this.likeStatus == LikeStatus.LIKE) {
            this.likeStatus = LikeStatus.UNLIKE;
        }
        if (this.likeStatus == LikeStatus.UNLIKE) {
            this.likeStatus = LikeStatus.CANCELED;
        }
        this.likeStatus = LikeStatus.UNLIKE;
    }
}

ReviewLike는 회원이 특정 게시물에 대해 좋아요, 싫어요와 같은 상태를 관리할 책임을 가졌습니다.
따라서, updateLike() 혹은 updateUnlike()를 통해 게시물의 상태만을 관리하게끔 하였습니다.

Service Layer

    @Transactional
    public void likePost(ReviewLikeRequest reviewLikeRequest) {
        ReviewLike reviewLike = reviewLikeRequest.toEntity();

        PerfumeReviewBoard perfumeReviewBoard = reviewBoardRepository.findByBoardId(reviewLike.getLikedPost().getBoardId())
                .orElseThrow(ReviewPostNotFoundException::new);

        reviewLike.updateLike();
        if (!isAlreadyPushLikeOrUnlike(reviewLikeRequest)) {
            reviewLikeRepository.save(reviewLike);
        }
        perfumeReviewBoard.likePost(reviewLike);
    }

    @Transactional
    public void unlikePost(ReviewLikeRequest reviewLikeRequest) {
        ReviewLike reviewLike = reviewLikeRequest.toEntity();

        PerfumeReviewBoard perfumeReviewBoard = reviewBoardRepository.findByBoardId(reviewLike.getLikedPost().getBoardId())
                .orElseThrow(ReviewPostNotFoundException::new);

        reviewLike.updateUnLike();
        if (!isAlreadyPushLikeOrUnlike(reviewLikeRequest)) {
            reviewLikeRepository.save(reviewLike);
        }
        perfumeReviewBoard.unlikePost(reviewLike);
    }

우선, isAlreayPushLikeOrUnlike()는 현재 Like 혹은 Unlike 상태값을 갖고 있는지 판단하는 메서드입니다.
회원이 처음 리뷰글에 반응을 한다면 이 반응에 따라 Db에 저장을 해야 합니다. 하지만, 이미 저장되어있는 반응을 수정할 경우에는 isAlreadyPushLikeOrUnlike()메서드를 통해 저장 작업은 건너뛰고 상태만을 update만을 합니다.

이 메서드는 reviewLike의 상태를 업데이트, 저장하고 PerfumeReviewBoard의 상태를 업데이트 합니다.

데이터의 변경 시에 트랜잭션 내에서 하나라도 실패하면 롤백해야 하기 때문에 @Transactional로 묶어주었습니다.

도메인 테스트

객체의 책임을 분리하니 테스트코드 작성이 간결해졌습니다.

Before

    @DisplayName("이미 좋아요가 달려있을 경우 좋아요가 취소된다.")
    @Test
    public void likePost() {

        PerfumeReviewBoard reviewPost = PerfumeReviewBoard.builder()
                .boardId(1L)
                .build();

        ReviewLike nonStatus = ReviewLike.builder()
                .reviewId(1L)
                .likedPost(reviewPost)
                .build();

        nonStatus.updateLike();
        Assertions.assertAll(
                //아무 상태가 아닐 경우 좋아요 수가 늘어난다.
                () -> Assertions.assertEquals(1L, nonStatus.getLikedPost().getLikeCount())
        );
    }
    @DisplayName("좋아요를 취소하면 좋아요 수가 줄어든다.")
    @Test
    void cancelLike() {
        PerfumeReviewBoard reviewPost = PerfumeReviewBoard.builder()
                .boardId(1L)
                .build();

        ReviewLike expectedCase = ReviewLike.builder()
                .reviewId(1L)
                .likedPost(reviewPost)
                .likeStatus(LikeStatus.LIKE)
                .build();

        ReviewLike alreadyLikeStatus = ReviewLike.builder()
                .reviewId(1L)
                .likedPost(reviewPost)
                .likeStatus(LikeStatus.LIKE)
                .build();

        alreadyLikeStatus.updateLike();
        Assertions.assertAll(
                () -> Assertions.assertEquals(LikeStatus.CANCELED, alreadyLikeStatus.getLikeStatus()),
                () -> Assertions.assertEquals(-1L, expectedCase.getLikedPost().getLikeCount())
        );
    }

좋아요의 상태 변경 + 각 경우에 따라 likeCount의 상태 변경까지 모두 테스트에 반영해야 했습니다.
작은 기능이기 때문에 쉽게 작성하긴 했지만 만약 더 복잡하게 꼬여있는 기능이었다면 분명 테스트하기 어려웠을 것입니다.

After

    @DisplayName("게시글을 Like상태로 전환한다. 이미 Like상태일 경우 Canceled상태로 전환한다.")
    @Test
    void updateLike(){
        ReviewLike statusLike = ReviewLike.builder()
                .likeStatus(LikeStatus.LIKE)
                .build();

        ReviewLike statusNothing = ReviewLike.builder()
                .likeStatus(null)
                .build();

        ReviewLike statusUnlike = ReviewLike.builder()
                .likeStatus(LikeStatus.UNLIKE)
                .build();

        Assertions.assertAll(
                //Unlike 상태일 경우 -> Like상태로
                () -> Assertions.assertEquals(LikeStatus.LIKE, statusUnlike.updateLike()),
                //Like 상태일 경우 -> Canceled상태로
                () -> Assertions.assertEquals(LikeStatus.CANCELED, statusLike.updateLike()),
                //아무 상태도 아닐 경우 -> Like상태로
                () -> Assertions.assertEquals(LikeStatus.LIKE, statusNothing.updateLike())
        );
    }

코드는 일부만 올렸습니다.

profile
기록하면서 성장하기!

0개의 댓글