우아한 테크코스 팀 프로젝트(속닥속닥)에서 JPA의 orphanRemoval을 사용하다가 겪은 문제입니다.
@Entity
@EntityListeners(AuditingEntityListener.class)
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "comment_id")
private Long id;
//...
@OneToMany(mappedBy = "comment", cascade = CascadeType.REMOVE, orphanRemoval = true)
private List<CommentLike> commentLikes = new ArrayList<>();
//...
protected Comment() {
}
@Getter
@Entity(name = "comment_likes")
public class CommentLike {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "comment_likes_id")
private Long id;
@ManyToOne
@JoinColumn(name = "comment_id")
private Comment comment;
@ManyToOne
@JoinColumn(name = "member_id")
private Member member;
// ...
public boolean isLikeOf(Long memberId) {
return member.hasId(memberId);
}
}
위와 같이 CommentLike와 Comment는 N:1의 연관 관계를 가지고 있습니다.
JPA 표준에서 Comment이 참조하고 있는 List 에서 하나의 CommentLike가 삭제되고 고아(orphan)가 된다면 해당 CommentLike는 삭제가 됩니다.
orphanRemoval이 잘 동작하는지 확인하기 위해 아래와 같은 테스트 코드를 작성했습니다.
@Test
void flipPostLike_delete() {
// member1, member2, post 생성
Comment comment = Comment.parent(member2, post, "nickname", "댓글내용");
commentRepository.save(comment);
CommentLike commentLike = CommentLike.builder()
.comment(comment)
.member(member)
.comment(comment)
.build();
commentLikeRepository.save(commentLike);
Comment foundComment = commentRepository.findByIdForCommentLike(comment.getId())
.orElseThrow(CommentNotFoundException::new);
// CommentLike를 orphan으로 만들기
List<CommentLike> commentLikes = foundComment.getCommentLikes();
commentLikes.remove(commentLike);
entityManager.flush(); // orphanRemoval은 flush 시점에 실행되기 때문에 강제 flush
}
JPA 표준에는 orphan이 되면 flush 시점에 해당 엔티티에 대한 delete Query가 실행된다고 되어 있지만, delete Query가 실행되지 않았습니다.
이와 관련해서 구글링을 해본 결과, 아래의 글에서 해답을 얻을 수 있었습니다.
https://github.com/jyami-kim/Jyami-Java-Lab/issues/1
바로, orphanRemoval이 CASCADE 속성에 의존적이라는 것입니다.
위의 Comment를 보시면, Comment의 CommentLike에는 CASCADE 속성이 REMOVE로 지정되어 있습니다.
CASCADE 속성을 ALL 혹은 PERSIST로 지정하면, orphanRemoval이 잘 작동합니다. 또한, CASCADE 속성이 지정되어 있지 않다면 동작하지 않습니다.
JPA 표준에 따르면, CASCADE 속성과 orphanRemoval은 독립적이지만 Hibernate 구현체에서는 CASCADE의 영향을 받는 것으로 보입니다.
참고로, Hibernamte 버전은 아래와 같습니다.
!
비슷한 느낌이라도 orphanRemoval과 CASCADE.PERSIST는 엄연히 다른 맥락에서 사용되고 다른 기능이라고 생각합니다. 따라서, 둘은 독자적으로 지정되어야 한다고 생각합니다.
https://www.inflearn.com/questions/137740
인프런 질문 게시판에 김영한 님의 답글을 보니, orphanRemoval의 경우에 주인 엔티티가 하위 엔티티를 관리하는 경우에 orphanRemoval과 CASCADE.PERSIST를 함께 사용하기 때문에 큰 문제가 되지 않는다는 글이 있으니 고민하실 때, 참고하셔도 좋을 것 같습니다.
끗
https://github.com/jyami-kim/Jyami-Java-Lab/issues/1
https://www.inflearn.com/questions/137740
https://hibernate.atlassian.net/browse/HHH-6709
우아한테크코스 4기 오찌(오지훈), 필즈(조성우)
정말 골때리는 버그.. 심지어 저거 수정했다가 더 큰 문제 생겨서 롤백했다니...