Post와 Comment가 양방향 연관관계를 가지고 있다. 그리고 Postman으로 댓글 작성 후 다시 댓글 삭제 요청을 보내니, success 응답이 돌아왔다(에러로그도 전혀 뜨지 않았다). 그런데 이때, 댓글 조회를 요청해보니 댓글이 그대로 남아있었다. DB를 확인해보니 마찬가지로 남아있는 상태였다.
처음엔 삭제 연관관계 편의 메서드가 없기 때문인 줄 알았다. 다시 말해 CommentRepository에서 delete를 해주어 DB에서는 삭제가 되었지만, Post의 List<Comment>에는 remove를 해주지 않아 자바 객체에는 남아있는 것이 원인이라고 생각했다.
하지만 알고보니 JPA가 객체상태를 알아서 관리해주므로, 삭제 연관관계 편의 메서드는 작성해주지 않아도 상관없었다. (양방향 연관관계에서 CommentRepository를 통해 delete()를 호출하면, 영속성 컨텍스트가 연관 엔터티 간의 상태 변화를 자동으로 동기화하기 때문에, Post의 List<Comment>에서도 해당 댓글이 자동으로 제거된다고 한다)(추가할 때와는 달리 삭제할 땐, 연관관계 편의 메서드가 필요하지 않음 _cascade옵션 또는 orphanRemoval이 설정이 되어 있다면)
작성한 코드에 문제가 있었다. 그 코드와 함께 설명해보겠다.
(빠른 이해를 위해, 다소 수정한 코드이다.)
class CommentService{
@Transactional
public void delete(Comment comment) {
commentRepository.delete(comment); //1번
post.decreaseCommentCount(); //2번
}
}
1번 라인에서 Comment를 delete하는 쿼리가 쿼리저장소에 저장된다.
그리고 2번 라인의 메서드를 통해 해당 Comment를 다시 사용하게 된다.
(아직 트랜잭션이 끝나지 않아서 삭제쿼리가 나가지 않았으므로, 해당 Comment를 사용할 수 있다.)
2번 라인이 어떻게 Comment를 사용한다는 말인지는 아래 코드를 보자.
class Post{
List<Comment> comments = new LinkedList<>();
Long commentCount;
public void decreaseCommentCount(){
commentCount = comments.size();
}
}
decreaseCommentCount() 메서드에서 List<Comment>의 size()를 계산하기 위해, List를 탐색하며 해당 Comment에 접근(사용) 하게 된다.
이 때, 이 Comment를 사용하게 되므로 JPA가 도로 저장시켜버린다.
그리고 트랜잭션이 끝나게 되어, 삭제가 반영되지 않게 된 것이라고 한다.
요약하면 한 트랜잭션 안에서 Comment를 삭제하고나서, 해당 트랜잭션이 끝나기 전에 그 Comment를 다시 사용한 것이 문제라고 할 수 있다.
다음과 같이 Comment를 Repository를 통해 삭제한 후, 그 트랜잭션 안에서 다시 그 Comment 객체를 사용하지 않도록 수정되었다.
public void decreaseCommentCount(){
commentCount--;
}
원래 Post의 댓글 갯수를 나타내는 commentCount 필드의 정합성을 보장하기 위해, 매번 List<Comment>의 size()를 직접 계산해주었다. 하지만 Comment의 갯수를 굳이 Post의 필드로 만들어 반정규화를 한 취지에 어긋나므로, 위와 같이 로직을 수정함으로써 문제의 원인도 함께 해결되었다.
아래 PR을 통해 자세한 상황을 알 수 있다.
[#41] [FEATURE] 게시글의 댓글 기능 구현 PR
이와 관련된 정확한 개념이 부족한 것 같아서 추가로 찾아보고 정리한 내용이다.
[TIL] 양방향 연관관계 다루기 feat.JPA
[TIL] 영속성 관리