[스프링부트+JPA+queryDsl] 대댓글(계층형) 구현
해당글의 엔티티부분을 참고했으나 나머지 부분들은 제 입맛에 맞게 수정한 코드입니다. 특정한 부분이 포함이 되어있지 않을 수 있으나 저같이 초보 개발자들이 따라하기 나쁘지 않은 로직을 정리해 기록하기 위해 작성한 글입니다.
단순히 복잡한 로직을 구현할 필요 없이 타임리프로 어떠한 댓글은 숨기고 어떠한 댓글은 출력하는 방식으로 대댓글 입력/출력을 구현해보자.
해당 방식은 대댓글에도 대댓글을 등록할 수 있는게 아닌 댓글에 대댓글만 등록할 수 있는 로직을 구현한다.
기존의 댓글 하나만 입력할 수 있던 로직에서 연관관계를 설정할 부모 댓글 컬럼을 생성해주고, 댓글들을 불러올때 필요한 자식 댓글을 담아줄 HashSet, 그리고 부모댓글인지 확인해줄 isParent 컬럼을 생성하자.
댓글을 등록할때는 대댓글이 아니라면 isParent 를 Y 으로 설정해 값을 넣어주고
대댓글일때는 N를 입력해 값을 넣어줘 타임리프에서 댓글 사이에 대댓글이 들어가는 형태로 뷰를 작성할 계획이다.
@Setter
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_id")
private ArticleComment parent;
@Setter
@OneToMany(mappedBy = "parent")
@ToString.Exclude
private Set<ArticleComment> children = new LinkedHashSet<>();
@Setter
private String isParent;
기존에 생성해놨던 댓글 엔티티에 이렇게 대댓글 로직을 위한 멤버변수들을 선언해준다.
부모댓글의 경우에는 여러개의 답글이 하나의 댓글로 연관관계가 설정될것이니 @ManyToOne 어노테이션을 사용해준다.
자식댓글의 경우에는 무한참조가 발생될 수 있기 때문에 @ToString.Exclude 어노테이션을 사용해주고, 자식댓글또한 여러개의 답글이 하나의 댓글에 등록될것이니 원투매니를 사용해준다.
기존에 만들어둔 dto는 대댓글을 출력할때 필요한 isParent 와 children set 을 전달해주지 않으니 차례대로 수정을 해주자.
public record ArticleCommentDto(
Long id,
Long articleId,
UserAccountDto userAccountDto,
String content,
LocalDateTime createdAt,
String createdBy,
LocalDateTime modifiedAt,
String modifiedBy,
String deleted,
Set<ArticleCommentDto> children,
String isParent
) {
public static ArticleCommentDto of(Long articleId, UserAccountDto userAccountDto, String content, Set<ArticleCommentDto> children,String isParent) {
return new ArticleCommentDto(null, articleId, userAccountDto, content, null, null, null, null, null, children, isParent);
}
public static ArticleCommentDto of(Long id, Long articleId, UserAccountDto userAccountDto, String content, LocalDateTime createdAt, String createdBy, LocalDateTime modifiedAt, String modifiedBy,String deleted, Set<ArticleCommentDto> children,String isParent) {
return new ArticleCommentDto(id, articleId, userAccountDto, content, createdAt, createdBy, modifiedAt, modifiedBy, deleted, children, isParent);
}
public static ArticleCommentDto from(ArticleComment entity) {
return new ArticleCommentDto(
entity.getId(),
entity.getArticle().getId(),
UserAccountDto.from(entity.getUserAccount()),
entity.getContent(),
entity.getCreatedAt(),
entity.getCreatedBy(),
entity.getModifiedAt(),
entity.getModifiedBy(),
entity.getDeleted(),
entity.getChildren().stream()
.map(ArticleCommentDto::from)
.collect(Collectors.toCollection(LinkedHashSet::new)),
entity.getIsParent()
);
}
기존에 deleted 까지만 있던 부분에서 children 과 isParent 가 추가되었다.
자식댓글은 기존에 엔티티이므로 dto로 다시 변환해 linkedHashSet으로 전달해준다.
마찬가지로 reponse dto 에 isParent 와 children 을 추가해주고, request 에는 부모댓글 등록에 필요한 ParentId 를 추가해주면 된다.
public record ArticleCommentRequest(
Long articleId,
String content,
Long parentId) implements Serializable {
public static ArticleCommentRequest of(Long articleId, String content) {
return new ArticleCommentRequest(articleId, content, null);
}
public static ArticleCommentRequest of(Long articleId, String content, Long parentId) {
return new ArticleCommentRequest(articleId, content, parentId);
}
public ArticleCommentDto toDto(UserAccountDto userAccountDto) {
return ArticleCommentDto.of(
articleId,
userAccountDto,
content,
null,
null
);
}
}
request 같은 경우에는 댓글을 등록할때 children, isParent 를 설정할것이기 때문에 null로 전달해도 상관없다. (사실 이 부분은 구조적으로 결함이 있는것 같다. dto 에서 해당 부분을 입력받지 않도록 수정해도 된다.)
이제 기존에 만들어둔 서비스클래스를 수정할 차례이다. 대댓글이 아니라면 isParent 에 Y를 입력해 등록하고, 대댓글이라면 isParent에 N을 입력하고 따로 부모댓글을 등록하고 부모댓글의 children 에 대댓글을 넣어줘야하기 때문에 메소드를 따로 선언해주었다.
저는 TDD방식으로 차근차근 코드를 작성해나갔으나 이 글은 구현 순서를 기록해두는 글이므로 테스트코드 작성은 패스하겠습니다.
public void saveArticleComment(ArticleCommentDto dto) {
try {
Article article = articleRepository.getReferenceById(dto.articleId());
ArticleComment comment = articleCommentRepository.save( dto.toEntity(article,dto.userAccountDto().toEntity()));
comment.setIsParent("Y");
} catch (EntityNotFoundException e) {
log.warn("댓글 저장 실패. 댓글 작성에 필요한 정보를 찾을 수 없습니다 - {}", e.getLocalizedMessage());
}
}
public void saveChildrenComment(Long parentId, ArticleCommentDto children) {
try {
ArticleComment parent = articleCommentRepository.getReferenceById(parentId);
ArticleComment articleComment = children.toEntity(parent.getArticle(),children.userAccountDto().toEntity());
articleCommentRepository.save(articleComment);
articleComment.setIsParent("N");
articleComment.setParent(parent);
Set<ArticleComment> set = parent.getChildren();
set.add(articleComment);
parent.setChildren(set);
} catch (EntityNotFoundException e) {
log.warn("댓글 저장 실패. 댓글 작성에 필요한 정보를 찾을 수 없습니다 - {}", e.getLocalizedMessage());
}
}
이렇게 부모댓글 저장용 메소드와 자식댓글 저장용 메소드를 따로 선언한다.
*jpa는 따로 수정해 다시 세이브를 하지 않고 set 만 해줘도 자동으로 업데이트가 된다.
이제 따로 대댓글 입력을 위한 컨트롤러 메소드를 선언해주면 된다.
@PreAuthorize("isAuthenticated()")
@PostMapping("/{articleId}/reply")
public String writeChildrenComment(@PathVariable Long articleId,ArticleCommentRequest dto, @AuthenticationPrincipal BoardPrincipal principal) {
if (principal.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_USER"))) {
articleCommentService.saveChildrenComment(dto.parentId(),dto.toDto(principal.toDto()));
return "redirect:/articles/" + articleId;
}
return "redirect:/articles/" + articleId;
}
댓글,대댓글을 입력하기 위해서는 인증이 이미 되어있어야하기 때문에 인터셉터를 위해 @PreAuthorize 어노테이션을 사용해준다.
이러면 끝일줄 알았지?~~~ 타임리프로 구현하는게 더 어려웠다.
다음글에서 이어집니다.