orphanRemoval이 동작하지 않는다면

공병주(Chris)·2022년 10월 11일
1
post-thumbnail

orphanRemoval이 동작하지 않는다면

우아한 테크코스 팀 프로젝트(속닥속닥)에서 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
}

delete Query 실행되지 않음

JPA 표준에는 orphan이 되면 flush 시점에 해당 엔티티에 대한 delete Query가 실행된다고 되어 있지만, delete Query가 실행되지 않았습니다.

CASCADE 속성에 의존적인 orphanRemoval

이와 관련해서 구글링을 해본 결과, 아래의 글에서 해답을 얻을 수 있었습니다.

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 대체 뭘까..?

CASECADE에 의존적인데 이게 맞을까…?

비슷한 느낌이라도 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기 오찌(오지훈), 필즈(조성우)

4개의 댓글

comment-user-thumbnail
2022년 10월 11일

정말 골때리는 버그.. 심지어 저거 수정했다가 더 큰 문제 생겨서 롤백했다니...

1개의 답글
comment-user-thumbnail
2023년 8월 27일

정말.... 감사합니다...덕분에 해결책을 찾았습니다ㅜㅜ

1개의 답글