✨개요
🏃 목표
📢 댓글을 삭제하는 기능을 구현하자.
📜 접근방법
✅ 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에 대해 섬세하게 공부할 필요성을 느끼게 되었다.
📄 참고