[Project] 좋아요 기능 구현하기

김지현·2023년 11월 23일
2

Spring Boot 프로젝트

목록 보기
4/20

[Spring Boot] 뉴스피드 프로젝트에 좋아요 기능 구현하기

Post에 대한 CRUD 기능 구현 완료 후 좋아요 기능을 구현해보았다. 이 기능은 다음 사항을 고려해야 한다.

  1. 유저는 하나의 포스트에 대해 한 번만 좋아요 할 수 있으며 이미 좋아한 게시글에 다시 좋아요를 누르면 취소된다.
  2. 포스트는 좋아요 개수를 반환해야 한다.
  3. 자신의 포스트에 좋아요 할 수 없다.

첫 번째 방식

우선적으로 생각한 방식은 하나의 API 요청에 좋아요와 좋아요 취소 기능을 모두 구현하는 것이다. LikesRespository에 좋아요가 있는지를 탐색 후, 있을 경우 취소 없을 경우 생성을 하는 방식이다.

Entity

우선 엔티티 설계를 고민하였는데 유저는 여러 개의 좋아요를 할 수 있고 포스트도 여러 개의 좋아요를 받을 수 있으므로 n:m 관계가 된다. 이를 효과적으로 처리하기 위해 likes 테이블을 추가로 설계하여 유저, 포스트와 각각 1:n 매핑을 해주었다.

@Entity
@Table(name = "likes")
public class Likes {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "like_id")
    private Long id;

    @ManyToOne
    @JoinColumn(name = "user_id")
    private User user;

    @ManyToOne
    @JoinColumn(name = "post_id")
    private Post post;
}

likes 엔티티를 따로 생성하였고 url도 /likes로 설정했기 때문에 컨트롤러, 서비스, 리포지토리도 모두 포스트와 분리하여 구현하였다.

Controller

좋아요를 하기 위해서는 해당 포스트에 대한 정보(postid), 그리고 유저 정보가 필요하므로 매개변수로 받아왔다. 서비스에서 포스트에 대한 좋아요 로직을 수행하는데 좋아요가 되어있지 않은 게시물일 경우 좋아요를 누르고 true를 반환하고 아닐 경우 좋아요를 취소하고 false를 반환한 뒤, 반환 값에 따라 다른 응답을 보내도록 처리하였다. 또한 좋아요 개수를 responsedto에 담아 반환하도록 하였다.

@PutMapping
    public ResponseEntity<BaseResponse<LikesResponseDto>> likePost(@PathVariable Long postId, @AuthenticationPrincipal UserDetailsImpl userDetails){
        Boolean isLiked = likesService.likePost(postId, userDetails.getUser());
        LikesResponseDto dto = (new LikesResponseDto(likesService.countLikes(postId)));
        String message = isLiked ? "좋아요가 완료 되었습니다." : "좋아요가 취소 되었습니다.";
        return ResponseEntity.ok(BaseResponse.of(message, HttpStatus.OK.value(), dto));
    }

Service

서비스에서 좋아요 대한 로직을 구현한다. 우선 해당 유저가 해당 포스트에 대해 이미 좋아요를 눌렀는지 확인하기 위해 레포지토리에서 find한다. 찾은 값이 없을 경우, 즉 좋아요를 누르지 않았을 경우, 포스트를 찾아 반환하고 -> 본인이 작성한 포스트인지 확인하고 -> 좋아요 객체를 레포지토리에 저장하고 -> true를 리턴한다. 좋아요를 누른 경우 레포지토리에서 해당 좋아요 객체를 삭제하고 -> false를 리턴한다.

 public Boolean likePost(Long postId, User user) {
        Optional<Likes> like = likesRepository.findByPostIdAndUserId(postId, user.getId());

        if(like.isEmpty()){
            Post post = findPost(postId);
            checkUser(post, user);
            likesRepository.save(new Likes(post, user));
            return true;
        } else{
            likesRepository.delete(like.get());
            return false;
        }
    }

좋아요의 개수를 구하는 로직은 해당 포스트에 존재하는 likesList를 활용해 size를 구하여 반환하도록 했다. 이렇게 구현한 이유는 likescount에 대한 변수 선언이나 증가/감소 메서드를 따로 설정해주지 않고 size 반환만으로 간단하게 좋아요수를 구할 수 있다고 생각했기 때문이다. 따라서 좋아요수는 Post 엔티티안에 외래키 매핑으로 @OneToMany 애너테이션이 붙은 likesList의 size를 구해 반환하는 메서드를 구현하는 방법으로 구했다.

public int countLikes(Long postId) {
        Post post = findPost(postId);
        return post.getLikesCount();
    }
    
...

public int getLikesCount(){
        return likesList.size();
    }

또한 이 메서드를 포스트 조회 시에도 활용하여 responseDto에 좋아요 수를 담아 함께 반환해주도록 하였다.

두 번째 방식

이렇게 구현한 뒤 다른 팀원의

좋아요 완료 API와 좋아요 취소 API를 구분하는 것은 어떨까요?

라는 피드백이 있었다. 나는 좋아요 버튼과 좋아요 취소 버튼을 같은 버튼으로 생각하여 첫 번째 방식과 같이 설계하였는데 팀원의 경우 프론트에서 좋아요 버튼의 상태 값이 다를 경우 다른 API를 요청할 수 있으므로 두 번째 방식으로의 구현을 제안한 것이다. 실제로 조언을 구하였더니 두 번째 방식이 더 좋다고 한다. 백엔드에서는 한 가지 동작에 대해서는 한 가지 API만을 가지고있고 프론트에서 좋아요 버튼의 상태에 따라 다른 요청을 보내는 것이 더 RESTful한 설계가 된다고 하셨다.
그래서 아래와 같이 수정하였다.

Controller

완료는 PostMapping으로, 취소는 DeleteMapping으로 분리하였다.

@PostMapping("/likes")
    public ResponseEntity<BaseResponse<LikesResponseDto>> likePost(@PathVariable Long postId,
                                                                   @AuthenticationPrincipal UserDetailsImpl userDetails){
        LikesResponseDto dto = (new LikesResponseDto(likesService.likePost(postId, userDetails.getUser())));
        return ResponseEntity.ok(BaseResponse.of("좋아요가 완료 되었습니다.", HttpStatus.OK.value(), dto));
    }

    @DeleteMapping("/likes")
    public ResponseEntity<BaseResponse<LikesResponseDto>> unlikePost(@PathVariable Long postId,
                                                                   @AuthenticationPrincipal UserDetailsImpl userDetails){
        LikesResponseDto dto = (new LikesResponseDto(likesService.unlikePost(postId, userDetails.getUser())));
        return ResponseEntity.ok(BaseResponse.of("좋아요가 취소 되었습니다.", HttpStatus.OK.value(), dto));
    }

Service

서비스 부분도 if문 로직 부분을 서로 다른 메서드로 분리하였다.

public Integer likePost(Long postId, User user) {
        Post post = findPost(postId);
        checkUser(post, user);
        likesRepository.save(new Likes(post, user));
        return post.getLikesCount();
    }

    public Integer unlikePost(Long postId, User user) {
        Post post = findPost(postId);
        Likes like = likesRepository.findByPostIdAndUserId(postId, user.getId());
        likesRepository.delete(like);
        return post.getLikesCount();
    }

문제와 해결

그러나 다른 문제가 발생하였는데.... 좋아요 개수 변경이 제대로 이루어지지 않는다는 것이었다. 좋아요 레포지토리에서 추가와 삭제는 잘 이루어졌는데 좋아요 개수의 업데이트가 좋아요 삭제시 제대로 이루어지지 않았다. @Transaction 애너테이션이 있음에도 변경 사항이 반영되지 않아 우선은 로직을 변경하였다. 좋아요 개수를 DB에 저장하므로 likeCounts를 직접 더하고 빼는 것으로 수정하였더니 잘 동작하였다.
추후 코드를 다시 분석하였더니 리스트 사이즈 반환 로직이 제대로 동작하지 않았던 이유는 delete 후에 다시 DB를 읽어오는 과정이 없어서 이전 리스트의 사이즈를 반환한 것 같다. 다만 변경된 로직이 더 명확하고 간단하므로 그냥 직접 더하고 빼는 로직을 유지하기로 하였다.

post.increaseLikes();

private void increaseLikes(){
	this.countLikes++;
}

결과

다음과 같이 잘 동작한다!

포스트 조회 시에도 좋아요 수를 함께 반환한다.

0개의 댓글