Develop

[Spring Boot] 게시판 - 03. 댓글(Comment) API 구현 (대댓글, 소프트 삭제) 본문

백엔드/게시판 만들기

[Spring Boot] 게시판 - 03. 댓글(Comment) API 구현 (대댓글, 소프트 삭제)

230801 2025. 11. 21. 01:45

안녕하세요  .ᐟ

게시판 프로젝트 3일차 입니다. (작심삼일도 여러번하면 결국엔 꾸준히 할 수 있습니다 화이팅)

 

오늘도 게시판의 핵심기능인 댓글대댓글을 구현했습니다. 

 

오늘의 학습 목표

  • Comment Entity 설계
  • 댓글 CRUD API 구현
  • 대댓글 계층 구조 구현

 


Entity 설계

연관관계 매핑

Comment는 세 가지 연관관계를 가집니다.

  • Comment → Post: @ManyToOne으로 게시글과 연결
  • Comment → User: @ManyToOne으로 작성자와 연결
  • Comment → Comment: Self-Referencing으로 부모-자식 관계 구현

 

 

Self-Referencing 구조

대댓글 구조를 위해 같은 테이블의 다른 레코드를 참조합니다.

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "parent_id")
private Comment parent;  // 부모 댓글

@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List children = new ArrayList<>();  // 자식 댓글들

 

 

이 구조를 통해 다음과 같은 계층을 표현 할 수 있습니다.

  • 무한 depth 의 댓글 구조를 단일 테이블로 구현 가능
  • 부모-자식 관계를 명확하게 표현
  • 대댓글 조회 시 parent.children 으로 간단히 접근 가능
댓글 (parent_id = null)
├─ 대댓글1 (parent_id = 1)
├─ 대댓글2 (parent_id = 1)
└─ 대댓글3 (parent_id = 1)

 

 

소프트 삭제

  • deleted 플래그를 사용해 실제 데이터를 삭제하지 않고 삭제 표시만 했습니다.
    • 대댓글이 있는 댓글의 맥락을 유지하기 위함입니다.
  • 소프트 삭제를 선택한 이유
    • 대댓글 맥락 유지
    • 데이터 복구
    • 히스토리 추적
  • 실제 삭제 vs 소프트 삭제 판단 기준
    • 대댓글이 있는 최상위 댓글 -> 소프트 삭제
    • 대댓글이거나 자식이 없는 댓글 -> 실제 삭제

 

Repository

  • Spring Data JPA 메서드 네이밍 규칙
  • @Query 사용

 

Service 로직

댓글 작성

  • parentId가 있으면 대댓글로 처리했습니다.
  • Builder 패턴으로 Comment 객체를 생성하고 저장했습니다.

 

댓글 조회

  • 최상위 댓글만 조회하거나, 모든 댓글을 조회하는 두 가지 메서드를 구현했습니다.
  • filter(Comment::isParentComment)로 최상위 댓글을 필터링했습니다.
  • Entity를 DTO로 변환해 반환했습니다.

 

댓글 수정

  • 작성자 권한을 검증했습니다
  • 삭제된 댓글은 수정할 수 없도록 처리했습니다
  • 변경 감지를 활용해 updateContent() 메서드만 호출했습니다.

 

 

댓글 삭제

대댓글의 맥락을 유지하기 위해 삭제를 분기처리 했습니다.

  • 자식이 있는 최상위 댓글: 소프트 삭제 (deleted = true)
  • 대댓글 또는 자식이 없는 댓글: 실제 삭제 (repository.delete())

 

Controller

  • /posts/{postId}/comments 경로로 댓글이 게시글에 종속적임을 URL로 표현했습니다.

DTO

  • Entity 를 직접 받지 않고 ID 만 받음 (순환 참조 방지)
  • paerintId는 선택적으로 받음 (일반 댓글일때는 null)
  • 삭제된 댓글은 "삭제된 댓글입니다"를 표시
  • 최상위 댓글일때만 children 포함

느낀 점

  • 계층 구조 데이터 설계
    • Self-Referencing으로 무한 depth를 구현할 수 있다는게 신기했습니다. 
    • 같은 테이블을 참조하는 방식이 어색하긴하지만 카테고리나 조직도에서도 활용할 수 있다고 하니 익숙해져야겠습니다.
  • @Builder.Default 사용법
    • 빌더 패턴 사용 시 List 나 boolean 같은 필드가 초기화 되지않는 문제가 간혹 발생하는데, 이때 @Builder.Default를 붙이면 명시적으로 기본값을 설정 할 수 있다는걸 배웠습니다.
  • 비즈니스 로직을 Entity에 두는 이유
    • 처음에는 Service 에서 로직을 처리하려고 했는데, 어제 Post Entity 에서 로직을 한번 짜본 뒤로 가독성이 좋다고 생각이들어서 이번에도 Entity에서 일부 로직을 작성했습니다.
    • Service에서 Entity의 내부 구조가 바뀌어도 Service 는 수정할 필요가 없어서 좋았습니다.