본문 바로가기

Project/DB

[Independe] 대댓글 설계에 대한 고민

자취생 커뮤니티를 개발하며 댓글과 대댓글에 대한 요구사항이 있었다. 난 단순히 댓글 테이블과 대댓글 테이블을 따로 두어 1 : N 관계를 맺으면 될것이라 판단했다. 그러나 구글링을 통해 다른 개발자분들이 대댓글을 구현한 것을 보니 모두 계층형 구조를 가져갔다. 계층형 구조 자체는 이해하기 수월했지만 구현된 코드를 보니 다소 이해하기 난해했다. 하지만 많은 개발자분들이 작성해주신 예제들을 이용해 직접 구현해보며 코드를 이해할 수 있었다.

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED) // protected 기본 생성자
public class Comment extends BaseEntity {

    @Id @GeneratedValue
    @Column(name = "comment_id")
    private Long id;

    @Column(columnDefinition = "text") // 텍스트 타입
    private String content;

    //== 계층형 댓글 ==//
    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "parent_id")
    private Comment parent;

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


    //== 연관 관계 ==//
    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "member_id")
    private Member member; // 댓글, 회원 N : 1 다대일 단방향 매핑

    @ManyToOne(fetch = LAZY)
    @JoinColumn(name = "post_id")
    private Post post; // 댓글, 게시 N : 1 다대일 단방향 매핑

    //== 생성 ==//
    @Builder
    public Comment(String content, Member member, Post post, Comment parent) {
        this.content = content;
        this.member = member;
        this.post = post;
        if(parent != null) {
            this.parent = parent;
            parent.getChild().add(this);
        }
    }
}

해당 코드에서 주목할 점은 //== 계층형 구조 ==// 이후 작성된 코드와 생성자이다. @ManyToOne을 이용해 Comment 테이블 내에 parent_id 컬럼을 추가해 자기 자신을 참조한다. 해당 컬럼을 이용해 부모 댓글의 정보를 알 수 있다. 또한 List 객체를 이용해 자신의 자식 댓글들을 저장할 수 있도록 구현한다. 이때 댓글이 지워질 때 댓글에 대한 자식 댓글들은 모두 지워지므로 orphanRemoval을 true로 설정한다. 

많은 개발자 분들은 구현 시 처음 댓글(1번)을 작성하고 댓글에 대댓글(2번)을 작성할 때 2번 댓글의 부모는 1번이 되고 대댓글에 또 다시 대댓글(3번)을 작성하면 3번의 부모는 2번이 되도록 구현하셨다. 하지만 난 굳이 이렇게 까지 구현해야 하나 싶은 생각이 강하게 들었다.

왜? 생각해보면 댓글(1번) 대댓글(2번)을 작성하면 2번 대댓글에 부모가 1번이 되는건 똑같지만 2번 대댓글에 대댓글(3번)을 작성하면 3번 대댓글의 부모는 1번 댓글이 되도 상관없다고 생각한다. 2번과 3번을 시간순으로만 저장한다면 1번 댓글 아래 2번 3번 대댓글이 작성된 구조는 똑같이 가져갈 수 있다고 생각한다. (아직 신입 개발자라 이해도가 낮아 내 생각이 잘못됐다면 이에대한 비판을 겸허히 받아드리겠다.)

생성자에 parent(부모댓글)가 넘어오는지 확인한 후 parent이 존재하면 parent 객체와 함께 parent 객체의 자식에 자신(this)를 저장하여 구현을 마무리했다.

@Query(value = "select c from Comment c left join fetch c.parent" +
            " join fetch c.member" +
            " where c.post.id = :postId" +
            " order by c.parent.id ASC nulls first, c.createdDate asc",
countQuery = "select c from Comment c" +
			" where c.post = :postId")
List<Comment> findAllByPostId(@Param("postId") Long PostId);

게시글 조회 시 게시글과 함께 작성된 댓글과 대댓글이 모두 조회되어야 하므로 게시글의 PK를 이용해 조회하며 부모가 없는 댓글을 먼저, 작성일을 오름차순으로 조회하여 프론트엔드 단에서 쉽게 댓글과 대댓글을 구분하여 사용자에게 보여줄 수 있도록 했다.

 

사실 아직까지 왜 댓글과 대댓글을 1 : N 관계로 구현하지 않고 계층형 구조로 저장하는지 완벽하겐 이해하지 못 했다. 혼자 고민해본 결과는 fetch join 만으로 해당 댓글에 대한 부모 댓글과 자식 댓글을 모두 파악할 수 있어 편리했다. 두 번째로 만약 대댓글 테이블을 만든다면 이 테이블은 Id(PK), comment_id(FK), created_date, last_modified_date, member_id, content 컬럼이 존재할 것이다. 사실 이는 댓글 테이블에서 comment_id(FK)만 추가된 형태이다. 즉, 대댓글 테이블을 만드는 것 자체가 매우 비효율적일 수도 있다고 생각했다.

 

* 저의 구현방식에 대한 의견과 더불어 계층형 구조에 대한 명확한 답변을 아시는 분들은 댓글을 달아주시면 정말 감사하겠습니다.