쇼핑몰 프로젝트에서 CRUD 및 페이징 기능과 댓글과 대댓글을 구현하고 싶어 커뮤니티 페이지를 구현하였습니다. 사실 많은 시행착오를 겪었지만, JPA로 엔티티 클래스 내부에서 계층형 데이터베이스를 만드는 것이 큰 경험이 되었기에 포스팅하기로 하였습니다.
댓글과 대댓글 기능을 구현하면서 JPA를 이용해 계층형 데이터베이스 구조를 어떻게 설계하고, 그에 따른 로직을 어떻게 구현했는지에 대해 설명할 것이고 실제로 구현된 코드와 함께 JPA의 장점을 살린 설계 방식을 중심으로 서술하겠습니다.
계층형 데이터베이스 설계 이론
계층형 구조는 데이터가 트리 구조를 형성하는 방식입니다. 트리 구조에서 부모 노드가 자식 노드를 가지는 것처럼, 댓글 시스템에서는 하나의 댓글이 다른 댓글의 부모가 될 수 있습니다. 이를 구현하기 위해 JPA에서는 자기참조 관계를 활용합니다.
부모 댓글: 상위 댓글로, 다른 댓글을 자식으로 가질 수 있습니다.
자식 댓글: 하위 댓글로, 특정 부모 댓글에 속해 있습니다.
@ManyToOne @OneToMany 어노테이션을 사용해 정의하고 이것은 JPA가 관계를 매핑하기 위해 제공하는 표준적인 방법입니다.
댓글 엔티티 설계
@Entity
@Table(name = "COMMENTS")
@Getter
@NoArgsConstructor
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "COMMENT_ID")
private Long id;
@Column(name = "CONTENT", nullable = false, length = 150)
private String content;
@CreationTimestamp
@Column(name = "CREATED_AT")
private LocalDateTime createdAt;
@ManyToOne
@JoinColumn(name = "MEMBER_ID", nullable = false)
private Member member;
@ManyToOne
@JoinColumn(name = "BOARD_ID", nullable = false)
private Board board;
@ManyToOne
@JoinColumn(name = "PARENT_COMMENT_ID")
private Comment parentComment; // 부모 댓글을 참조하는 필드
@OneToMany(mappedBy = "parentComment", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Comment> childComments = new ArrayList<>(); // 자식 댓글 리스트
}
자식 댓글들을 필드 리스트에 있으며, 부모 댓글은 해당 리스트를 통해 자신의 자식 댓글들을 확인하 수 있게 됩니다.
댓글 작성 서비스 로직
@Service
@RequiredArgsConstructor
@Transactional
public class CommentService {
private final CommentRepository commentRepository;
private final BoardRepository boardRepository;
private final MemberRepository memberRepository;
private final SessionManager sessionManager;
public CommentDTO createComment(Long boardId, CommentDTO commentDTO, String sessionId) {
// 세션 ID를 이용해 사용자의 이메일을 가져옴
String userEmail = sessionManager.getSesstion(sessionId);
// 부모 댓글이 있는지 확인
Comment parentComment = null;
if (commentDTO.getParentCommentId() != null) {
parentComment = commentRepository.findById(commentDTO.getParentCommentId())
.orElseThrow(() -> new CommentNotFoundException("부모 댓글을 찾을 수 없습니다"));
}
// 게시물과 작성자 정보 가져오기
Board board = boardRepository.findById(boardId)
.orElseThrow(() -> new BoardNotFoundException("게시물을 찾을 수 없습니다"));
Member member = memberRepository.findByUserEmail(userEmail);
// 새로운 댓글 엔티티 생성
Comment comment = commentDTO.toEntity(board, member, parentComment);
// 댓글 저장
Comment savedComment = commentRepository.save(comment);
// 저장된 댓글을 DTO로 변환하여 반환
return Comment.toDTO(savedComment);
}
}
댓글 삭제 로직
public void deleteComment(Long commentId, String sessionId) {
// 세션 ID를 이용해 사용자의 이메일을 가져옴
String userEmail = sessionManager.getSesstion(sessionId);
// 댓글 조회
Comment findComment = commentRepository.findById(commentId)
.orElseThrow(() -> new CommentNotFoundException("댓글을 찾을 수 없습니다"));
// 삭제 권한 확인
if (!findComment.getMember().getUserEmail().equals(userEmail)) {
throw new UnauthorizedException("댓글을 삭제할 권한이 없습니다");
}
// 댓글 삭제
commentRepository.deleteById(commentId);
}
CascadeType.ALL 옵션으로 인해, 부모 댓글을 삭제하면 그와 연결된 모든 자식 댓글도 함께 삭제가 됩니다. 실제 쇼핑몰 환경에서도 부모 댓글 삭제 시, 밑의 댓글들도 삭제가 되기에 이를 보고 활용하였습니다.
마무리
이렇게 JPA를 활용하여 댓글과 대댓글의 계층형 구조를 설계하고, 부모-자식 댓글 간의 관계를 관리하는 방법을 터득할 수 있었습니다. 사실 아직까지 생소하다고 느껴지는 트리 구조에 대해 한 발자국 더욱 알아간 느낌이였는데요. 해당 구조를 추가로 더 정리를 해보면 트리 구조의 형태는 데이터를 표현할 때 매우 유용하며, 특히 현재 댓글 시스템과 같이 깊이 있는 데이터를 처리할 때 효과적이라는 것을 배웠습니다.
'spring' 카테고리의 다른 글
| AOP와 스프링 AOP란? (0) | 2024.09.23 |
|---|---|
| 스프링 세션과 Redis 응용 (1) | 2024.09.18 |
| [MileStone 프로젝트] Iamport를 사용하여 결제 시스템 구현 (0) | 2024.08.12 |
| [MileStone 프로젝트] EAGER // LAZY (로딩 전략) (0) | 2024.08.11 |
| [MileStone 프로젝트] SQL 쿼리 최적화와 성능 향상 (0) | 2024.08.07 |