✨개요
🏃 목표
📢 댓글을 삭제하는 기능을 구현하자.
📜 접근방법
✅ 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());
    }
📌 댓글 삭제 컨트롤러 구현
@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));
    }
@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();
    }
}
📌 댓글 삭제 서비스 구현
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);
        
        return CommentDeleteResponse.of("댓글 삭제 완료", findComment.getId());
    }
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());
        });
    }
📌 댓글 삭제 리포지토리 구현
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;
    }
}
@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에 대해 섬세하게 공부할 필요성을 느끼게 되었다.
📄 참고