요구사항
: 무한 대댓글 기능을 구현하고, 게시글 및 부모 댓글 삭제시 자식 댓글들 모두 삭제하기.
@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class Post extends Timestamped {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@Column(nullable = false)
private String content;
@OneToMany(fetch = FetchType.LAZY, mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Comment> comments;
@JoinColumn(name = "member_id", nullable = false)
@ManyToOne(fetch = FetchType.LAZY)
private Member member;
public void update(PostRequestDto postRequestDto) {
this.title = postRequestDto.getTitle();
this.content = postRequestDto.getContent();
}
public boolean validateMember(Member member) {
return !this.member.equals(member);
}
}
@OneToMany(fetch = FetchType.LAZY, mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)를 이용해 최상위 객체인 게시글이 삭제 되면 그 게시글에 등록되어있던 댓글들 모두 삭제가 되게끔 한다. 여기서 중요한건 mappedBy = "post"를 하지 않으면, 연관관계의 주인이 설정되지 않아 게시글을 삭제할경우 참조키 제약조건 위반으로 예외가 생긴다.
@Builder
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class Comment extends Timestamped {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@JoinColumn(name = "member_id", nullable = false)
@ManyToOne(fetch = FetchType.LAZY)
private Member member;
@JoinColumn(name = "post_id", nullable = false)
@ManyToOne(fetch = FetchType.LAZY)
private Post post;
@Column(nullable = false)
private String content;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_id")
private Comment parent;
@Builder.Default
@OneToMany(mappedBy = "parent", orphanRemoval = true)
private List<Comment> children = new ArrayList<>();
public void update(CommentRequestDto commentRequestDto) {
this.content = commentRequestDto.getContent();
}
// 부모 댓글 수정
public void updateParent(Comment parent){
this.parent = parent;
}
public boolean validateMember(Member member) {
return !this.member.equals(member);
}
}
Comment라는 엔터티 하나에 부모Comment와 자식Comment(List)를 넣어서 계층형 테이블로 구성한다.
1
2
3
7
4
5
6
이런 식의 구조가 될것이다.
3.댓글 저장
@Transactional
public ResponseDto<?> createComment(CommentRequestDto requestDto, HttpServletRequest request) {
Post post = checkMemberUtil.isPresentPost(requestDto.getPostId());
if (null == post) {
return ResponseDto.fail("NOT_FOUND", "존재하지 않는 게시글 id 입니다.");
}
Comment parent = null;
// 자식댓글인 경우
if(requestDto.getParentId() != null){
parent = checkMemberUtil.isPresentComment(requestDto.getParentId());
if (null == parent) {
return ResponseDto.fail("NOT_FOUND", "존재하지 않는 댓글 id 입니다.");
}
// 부모댓글의 게시글 번호와 자식댓글의 게시글 번호 같은지 체크하기
if(parent.getPost().getId() != requestDto.getPostId()){
return ResponseDto.fail("NOT_FOUND", "부모댓글과 자식댓글의 게시글 번호가 일치하지 않습니다.");
}
}
Comment comment = Comment.builder()
.member(member)
.post(post)
.content(requestDto.getContent())
.build();
if(null != parent){
comment.updateParent(parent);
}
commentRepository.save(comment);
CommentResponseDto commentResponseDto = null;
if(parent != null){
commentResponseDto = CommentResponseDto.builder()
.id(comment.getId())
.author(comment.getMember().getNickname())
.content(comment.getContent())
.createdAt(comment.getCreatedAt())
.modifiedAt(comment.getModifiedAt())
.parentId(comment.getParent().getId())
.build();
} else {
commentResponseDto = CommentResponseDto.builder()
.id(comment.getId())
.author(comment.getMember().getNickname())
.content(comment.getContent())
.createdAt(comment.getCreatedAt())
.modifiedAt(comment.getModifiedAt())
.build();
}
return ResponseDto.success(commentResponseDto);
}
API요청을 하나로 처리하기 때문에, 자식 댓글인지 부모댓글인지를 구분하며 Comment를 생성해 저장합니다.
CustomCommentRepository
@Repository
public class CommentCustomRepository {
private JPAQueryFactory jpaQueryFactory;
public CommentCustomRepository(JPAQueryFactory jpaQueryFactory) {
this.jpaQueryFactory = jpaQueryFactory;
}
// 게시글의 댓글 전체 가져오기
public List<Comment> findAllByPost(Post post){
return jpaQueryFactory.selectFrom(comment)
.leftJoin(comment.parent)
.fetchJoin()
.where(comment.post.id.eq(post.getId()))
.orderBy(comment.parent.id.asc().nullsFirst(), comment.createdAt.asc())
.fetch();
}
}
출력결과 예시
1 Null
6 Null
2 1
3 1
4 2
5 2
1) 댓글 조회가 가장 중요한 부분이라 이 부분은 자세하게 풀어서 설명을 해보겠다.
먼저 querydsl로 조회를 한다. 부모 댓글이 NULL이면 최상위 댓글이므로 가장 먼저 조회되기 위해 nullsFirst()를 사용했고 그 다음은 작성시간순으로 조회했다. 중요한것은 fetchJoin()을 통해 N+1을 해결할 수 있는데 자세한 내용은 https://cobbybb.tistory.com/18 을 참고하기.
위 결과를 대댓글의 중첩구조로 바꿔야 한다.
1의 자식들은 (2,3), 그 다음 2의 자식들은 (4,5) 그 다음 6이런 식으로 말이다.
@Transactional(readOnly = true)
public ResponseDto<?> getAllCommentsByPost(Long postId) {
Post post = checkMemberUtil.isPresentPost(postId);
if (null == post) {
return ResponseDto.fail("NOT_FOUND", "존재하지 않는 게시글 id 입니다.");
}
List<Comment> commentList = commentCustomRepository.findAllByPost(post);
List<CommentResponseDto> commentResponseDtoList = new ArrayList<>();
Map<Long, CommentResponseDto> map = new HashMap<>();
commentList.stream().forEach(c -> {
CommentResponseDto cdto = new CommentResponseDto(c);
if(c.getParent() != null){
cdto.setParentId(c.getParent().getId());
}
map.put(cdto.getId(), cdto);
if (c.getParent() != null) map.get(c.getParent().getId()).getChildren().add(cdto);
else commentResponseDtoList.add(cdto);
}
);
return ResponseDto.success(commentResponseDtoList);
}
2) List< CommentResponseDto > commentResponseDtoList = new ArrayList<>() 이 밑으로가 중첩구조로 바꾸는 기능을 하는데 풀이를 하자면, Map에는 임시로 response할 객체를 담는다.
그리고 자식댓글이면(부모댓글이 null이 아니면) map에서 부모댓글을 찾아서 자식댓글 리스트(children)에 추가해준다.
최상위댓글(parentId == null)이면 바로 리스트에 추가해준다.
삭제랑 수정은 매우 간단하니 패스!
좋은글 감사합니다^^