SNS 제작 (댓글 삭제)

개발연습생log·2023년 1월 5일
0

SNS 제작

목록 보기
9/15
post-thumbnail

✨개요

🏃 목표

📢 댓글을 삭제하는 기능을 구현하자.

📜 접근방법

  • 소프트 Delete
  • 연관관계에서 삭제

✅ TO-DO

  • 댓글 삭제 컨트롤러 테스트 구현
  • 댓글 삭제 서비스 테스트 구현
  • 댓글 삭제 컨트롤러 구현
  • 댓글 삭제 서비스 구현
  • 댓글 삭제 리포지토리 구현

🔧 구현

📌 댓글 삭제 컨트롤러 테스트 구현

댓글 삭제 성공

<@Test
    @DisplayName("댓글 삭제 성공")
    @WithMockUser
    void comment_delete_SUCCESS() throws Exception {

        when(commentService.delete("홍길동", 1L, 1L))
                .thenReturn(new CommentDeleteResponse("댓글 삭제 왼료", 1L));

        mockMvc.perform(delete("/api/v1/posts/1/comments/1")
                        .with(csrf())
                        .contentType(MediaType.APPLICATION_JSON))
                .andDo(print())
                .andExpect(status().isOk());
    }

댓글 삭제 실패

  • 유저가 없는 경우
@Test
    @DisplayName("댓글 삭제 실패1_유저가 없는 경우")
    @WithMockUser
    void comment_delete_FAILD_user() throws Exception {
        when(commentService.delete(any(), any(), any()))
                .thenThrow(new AppException(ErrorCode.USERNAME_NOT_FOUND, ErrorCode.USERNAME_NOT_FOUND.getMessage()));

        mockMvc.perform(delete("/api/v1/posts/1/comments/1")
                        .with(csrf())
                        .contentType(MediaType.APPLICATION_JSON))
                .andDo(print())
                .andExpect(status().isNotFound());
    }
  • 포스트가 없는 경우
@Test
    @DisplayName("댓글 삭제 실패1_포스트가 없는 경우")
    @WithMockUser
    void comment_delete_FAILD_post() throws Exception {
        when(commentService.delete(any(), any(), any()))
                .thenThrow(new AppException(ErrorCode.POST_NOT_FOUND, ErrorCode.POST_NOT_FOUND.getMessage()));

        mockMvc.perform(delete("/api/v1/posts/1/comments/1")
                        .with(csrf())
                        .contentType(MediaType.APPLICATION_JSON))
                .andDo(print())
                .andExpect(status().isNotFound());
    }
  • 작성자 불일치
@Test
    @DisplayName("댓글 삭제 실패3_작성자 불일치인 경우")
    @WithMockUser
    void comment_delete_FAILD_different() throws Exception {

        when(commentService.delete(any(), any(), any()))
                .thenThrow(new AppException(ErrorCode.INVALID_PERMISSION, ErrorCode.INVALID_PERMISSION.getMessage()));

        mockMvc.perform(delete("/api/v1/posts/1/comments/1")
                        .with(csrf())
                        .contentType(MediaType.APPLICATION_JSON))
                .andDo(print())
                .andExpect(status().isUnauthorized());
    }
  • 데이터베이스 에러
@Test
    @DisplayName("댓글 삭제 실패4_DB에러인 경우")
    @WithMockUser
    void comment_delete_FAILD_db() throws Exception {

        when(commentService.delete(any(), any(), any()))
                .thenThrow(new AppException(ErrorCode.DATABASE_ERROR, ErrorCode.DATABASE_ERROR.getMessage()));

        mockMvc.perform(delete("/api/v1/posts/1/comments/1")
                        .with(csrf())
                        .contentType(MediaType.APPLICATION_JSON))
                .andDo(print())
                .andExpect(status().isInternalServerError());
    }

📌 댓글 삭제 서비스 테스트 구현

댓글 삭제 성공

@Test
    @DisplayName("댓글 삭제 성공")
    void comment_delete_SUCCESS() {
        when(userRepository.findByUserName(any()))
                .thenReturn(Optional.of(user));
        when(postRepository.findById(any()))
                .thenReturn(Optional.of(post));
        when(commentRepository.findByPostIdAndId(any(), any()))
                .thenReturn(Optional.of(comment));

        assertDoesNotThrow(() -> commentService.delete(user.getUserName(), post.getId(), comment.getId()));
    }

댓글 삭제 실패

  • 유저가 없는 경우
@Test
    @DisplayName("댓글 삭제 실패1_유저가 존재하지 않는 경우")
    void comment_delete_FALID_user() {
        when(userRepository.findByUserName(any()))
                .thenReturn(Optional.empty());
        when(postRepository.findById(any()))
                .thenReturn(Optional.of(post));
        when(commentRepository.findByPostIdAndId(any(), any()))
                .thenReturn(Optional.of(comment));

        AppException exception = assertThrows(AppException.class, () -> commentService.delete(user.getUserName(), post.getId(), comment.getId()));
        assertEquals(ErrorCode.USERNAME_NOT_FOUND, exception.getErrorCode());
    }
  • 포스트가 없는 경우
@Test
    @DisplayName("댓글 삭제 실패2_포스트가 존재하지 않는 경우")
    void comment_delete_FALID_post() {
        when(userRepository.findByUserName(any()))
                .thenReturn(Optional.of(user));
        when(postRepository.findById(any()))
                .thenReturn(Optional.empty());
        when(commentRepository.findByPostIdAndId(any(), any()))
                .thenReturn(Optional.of(comment));

        AppException exception = assertThrows(AppException.class, () -> commentService.delete(user.getUserName(), post.getId(), comment.getId()));
        assertEquals(ErrorCode.POST_NOT_FOUND, exception.getErrorCode());
    }
  • 댓글이 없는 경우
@Test
    @DisplayName("댓글 삭제 실패3_댓글이 존재하지 않는 경우")
    void comment_delete_FALID_comment() {
        when(userRepository.findByUserName(any()))
                .thenReturn(Optional.of(user));
        when(postRepository.findById(any()))
                .thenReturn(Optional.of(post));
        when(commentRepository.findByPostIdAndId(any(), any()))
                .thenReturn(Optional.empty());

        AppException exception = assertThrows(AppException.class, () -> commentService.delete(user.getUserName(), post.getId(), comment.getId()));
        assertEquals(ErrorCode.COMMENT_NOT_FOUND, exception.getErrorCode());
    }
  • 작성자 불일치
@Test
    @DisplayName("댓글 삭제 실패4_작성자가 불인치인 경우")
    void comment_delete_FALID_different() {
        when(userRepository.findByUserName(any()))
                .thenReturn(Optional.of(user2));
        when(postRepository.findById(any()))
                .thenReturn(Optional.of(post));
        when(commentRepository.findByPostIdAndId(any(), any()))
                .thenReturn(Optional.of(comment));

        AppException exception = assertThrows(AppException.class, () -> commentService.delete(user2.getUserName(), post.getId(), comment.getId()));
        assertEquals(ErrorCode.INVALID_PERMISSION, exception.getErrorCode());
    }

📌 댓글 삭제 컨트롤러 구현

CommentController 메서드 추가

@DeleteMapping("/{id}")
    public ResponseEntity<Response> delete(Authentication authentication, @PathVariable Long postId, @PathVariable Long id){
        String userName = authentication.getName();
        CommentDeleteResponse commentDeleteResponse = commentService.delete(userName,postId,id);
        return ResponseEntity.ok().body(Response.of("SUCCESS",commentDeleteResponse));
    }

CommentDeleteResponseDTO 구현

@NoArgsConstructor
@AllArgsConstructor
@Builder
@Getter
@Setter
public class CommentDeleteResponse {
    private String message;
    private Long id;

    public static CommentDeleteResponse of(String message, Long id) {
        return CommentDeleteResponse.builder()
                .message(message)
                .id(id)
                .build();
    }
}

📌 댓글 삭제 서비스 구현

CommentService 메서드 추가

public CommentDeleteResponse delete(String userName, Long postId, Long id) {
        //유저 체크
        User findUser = AppUtil.findUser(userRepository, userName);
        //포스트 체크
        Post findPost = AppUtil.findPost(postRepository, postId);
        //댓글 체크
        Comment findComment = AppUtil.findComment(commentRepository, postId, id);
        //작성자 비교
        AppUtil.compareUser(findComment.getUser().getUserName(), userName);
        //삭제
        commentRepository.delete(findComment);
        //DTO 리턴
        return CommentDeleteResponse.of("댓글 삭제 완료", findComment.getId());
    }

AppUtil findComment 메서드 수정

public static Comment findComment(CommentRepository commentRepository, Long postId, Long commentId) {
        return commentRepository.findByPostIdAndId(postId, commentId).orElseThrow(() -> {
            throw new AppException(ErrorCode.COMMENT_NOT_FOUND, ErrorCode.COMMENT_NOT_FOUND.getMessage());
        });
    }

📌 댓글 삭제 리포지토리 구현

CommentRepository 메서드 추가

Optional<Comment> findByPostIdAndId(Long postId, Long commentId);

Base 엔티티 필드 추가

@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
@ToString
@Getter
public abstract class BaseEntity {

    @CreatedDate
    @Column(name = "createDate", updatable = false)
    private LocalDateTime createdAt;

    @LastModifiedDate
    @Column(name = "modifiedDate")
    private LocalDateTime modifiedAt;

    private LocalDateTime deletedAt;
}

Post 엔티티 논리 삭제 구현

@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Getter
@Where(clause = "deleted_at IS NULL")
@SQLDelete(sql = "UPDATE post SET deleted_at = CURRENT_TIMESTAMP where id = ?")
public class Post extends BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;
    private String body;

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

    @OneToMany(mappedBy = "post", fetch = FetchType.LAZY, orphanRemoval = true)
    private List<Comment> comments;

    public static Post of(String title, String body, User user) {
        return Post.builder()
                .title(title)
                .body(body)
                .user(user)
                .build();
    }

    public void modify(String title, String body) {
        this.title = title;
        this.body = body;
    }
}

Comment 엔티티 논리 삭제 구현

@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Getter
@Where(clause = "deleted_at IS NULL")
@SQLDelete(sql = "UPDATE comment SET deleted_at = CURRENT_TIMESTAMP where id = ?")
public class Comment extends BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String comment;

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

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

    public static Comment of(String comment, User user, Post post) {
        return Comment.builder()
                .comment(comment)
                .user(user)
                .post(post)
                .build();
    }

    public void modify(String comment) {
        this.comment = comment;
    }
}

🌉회고

  • JPA 연관관계에 대해 조금 더 감이 잡히게 되는 날이였다.
  • JPA의 옵션 하나하나가 DB운영에 큰 영향을 주기 때문에 JPA에 대해 섬세하게 공부할 필요성을 느끼게 되었다.

📄 참고

profile
주니어 개발자를 향해서..

0개의 댓글