스프링부트 JPA querydsl 대댓글(계층형 댓글) 기능 구현

미누·2023년 9월 17일

식구하자

목록 보기
4/5

해당 글은 트러블 슈팅(?)이라기 보다 , 식구하자 프로젝트에서 대댓글 기능 구현중 가장 많이 헤메고 어려움이 있었어서 대댓글 기능 구현과정을 공유할려고 포스팅 하려한다.


스프링 부트+JPA+querydsl을 활용하여 대댓글(계층형 댓글) 기능을 구현해보겠습니다.


@Getter
@Entity
@Table(name = "comment") 
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Comment {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "comment_id")
    private Long id; // 댓글 고유 번호
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    private Member member; // 댓글 작성자
    @Column(nullable = false)
    @Lob
    private String content; // 댓글 내용

    @Enumerated(value = EnumType.STRING)
    private DeleteStatus isDeleted;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "parent_id")
    private Comment parent;

    @OneToMany(mappedBy = "parent", orphanRemoval = true)
    private List<Comment> children = new ArrayList<>();

    public void changeDeletedStatus(DeleteStatus deleteStatus) {
        this.isDeleted = deleteStatus;
    }

}

일단 Comment 엔티티의 기본 구조는 위와 같습니다
id, 내용, 삭제된 상태, 작성자, 부모 댓글의 id값을 가지고 있고, OneToMany관계로 자식 댓글 리스트를 가지고 있습니다.

컬럼들은 각각 프로젝트의 맞춰 추가하시면 됩니다!

1
  2
    4
      6
    5
  3
    7
8

위와 같은 형태와 순서로 작성된 댓글이 있다고 가정하겠습니다.
각 댓글 컬럼은 부모 댓글의 id값을 가지고 있습니다.
따라서 DB에 저장된 ID와 부모댓글ID 값은 다음과 같을 것입니다!

1 NULL
2 1
3 1
4 2
5 2
6 4
7 3
8 NULL

1열은 각 댓글의 id값, 2열은 부모 댓글의 id값입니다.

계층 구조로 편하게 바꾸기 위하여 아래 코드를 이용하여, 부모 댓글 내림차순, 작성일자 내림차순으로 정렬하여 조회하겠습니다.

    @Override
    public List<Comment> findCommentByTradeBoardId(Long tradeBoardId) {
        return queryFactory.selectFrom(comment)
                .leftJoin(comment.parent)
                .fetchJoin()
                .where(comment.tradeBoardId.id.eq(tradeBoardId))
                .orderBy(
                        comment.parent.id.asc().nullsFirst()
                ).fetch();
    }

querydsl을 이용한 조회 코드입니다.
fetchJoin을 사용하여 N+1 문제를 방지하였습니다.
부모 댓글 컬럼이 NULL이라면, 최상위 댓글이므로 nullsFirst로 조회하였습니다.
그러면 기존 예시의 결과는 아래와 같을 것입니다.

1 NULL
8 NULL
2 1
3 1
4 2
5 2
7 3
6 4

댓글의 깊이 별로 구분되어 조회된 것을 확인할 수 있었습니다.
List로 반환된 위 결과를 이용하여 대댓글의 중첩구조로 바꿔낼 것입니다.
즉, 1의 자식 댓글은 (2, 3), 2의 자식 댓글은(4, 5), 4의 자식 댓글은 (6)...
이런 식으로 각 댓글DTO는 자신의 자식 댓글들을 가지게 됩니다.

⭐️중첩구조로 변환하는 코드는 아래와 같습니다.⭐️

    private List<CommentDto> convertNestedStructure(List<Comment> comments) {
        List<CommentDto> result = new ArrayList<>();
        Map<Long, CommentDto> map = new HashMap<>();
        comments.stream().forEach(c -> {
            CommentDto dto = CommentDto.convertCommentToDto(c);
            map.put(dto.getId(), dto);
            if(c.getParent() != null) map.get(c.getParent().getId()).getChildren().add(dto);
            else result.add(dto);
        });
        return result;
    }

조회 결과인 comments List는 깊이와 작성순으로 정렬된 결과를 가지고 있습니다.

정렬이 되어있기 때문에, 자식 댓글을 확인할 때는 부모 댓글이 이미 map에 들어가있는 상황입니다.
최상위 댓글이라면 result에 넣어주고, 부모가 있는 자식 댓글이라면 부모DTO의 자식 댓글 리스트로 add 해줍니다.
따라서 기존 예시로 보자면, result에는 1번과 8번 댓글이 담겨있고,
1번의 자식 댓글 리스트에는 2번과 3번 댓글이 담겨있고,
2번의 자식 댓글 리스트에는 4번과 5번 댓글이 담겨있는 형태의 중첩구조로 만들어질 것입니다.

추가로 이번에드 댓글 수정 기능을 만들어 보도록 하겠습니다.
마찬가지로 Querydsl을 사용하여 구현하였습니다

CommentRepository

  @Override
    public void updateComment(Comment comment) {
        queryFactory.update(QComment.comment)
                .where(QComment.comment.id.eq(comment.getId())) // 댓글 ID로 조건 설정
                .set(QComment.comment.content, comment.getContent()) // 댓글 내용 업데이트
                .execute();
    }
  • .where(QComment.comment.id.eq(comment.getId())):
    where 메소드를 사용하여 업데이트 대상을 선택합니다. 여기서는 댓글의 ID가 주어진 comment 객체의 ID와 일치하는 조건을 설정합니다.
  • .set(QComment.comment.content, comment.getContent()):
    set 메소드를 사용하여 업데이트할 필드와 값을 설정합니다. 여기서는 댓글의 내용을 주어진 comment 객체의 내용으로 업데이트합니다.
  • .execute():
    execute 메소드를 호출하여 쿼리를 실행하여 업데이트 작업을 수행합니다.

이 코드를 SQL 쿼리로 변환하면

UPDATE Comment
SET content = :newContent
WHERE id = :commentId;

위와 같습니다.

CommentService

   public CommentDto updateComment(CommentDto commentCreateRequestDto) {
        Comment comment = commentRepository.findById(commentCreateRequestDto.getId()).orElseThrow(EntityNotFoundException::new);
        comment.setContent(commentCreateRequestDto.getContent());
        commentRepository.updateComment(comment);
        return CommentDto.convertCommentToDto(comment);
    }

댓글의 업데이트 작업을 수행하고, 업데이트된 댓글의 DTO를 반환합니다. 먼저 주어진 댓글 ID로 해당 댓글을 찾고, 그 댓글의 내용을 주어진 DTO의 내용으로 업데이트한 후, 해당 댓글을 업데이트하는 메소드를 호출합니다. 마지막으로 업데이트된 댓글을 DTO로 변환하여 반환합니다!

위와 같은 형태로 대댓글 수정 코드도 구현할 수 있었습니다.

추가적인 전체 코드가 필요하시면 github 주소 공유드릴게요!
코드에 관한 피드백도 환영입니다

2개의 댓글

comment-user-thumbnail
2024년 3월 19일

안녕하세요 github 주소 공유해주시면 감사하겠습니다....!

1개의 답글