[삽질 로그] CascadeType.PERSIST가 없으면 orphanRemoval이 작동하지 않는 문제

woodyn·2021년 2월 8일
0

삽질 로그

목록 보기
4/5

문제 정의

@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString
@Entity
public class Child {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ToString.Exclude
    @ManyToOne
    private Mother mother;

}

@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString
@Entity
public class Mother {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ToString.Exclude
    @OneToMany(mappedBy = "mother", cascade = CascadeType.REMOVE, orphanRemoval = true)
    private Set<Child> children = new HashSet<>();

    public void addChild(Child child) {
        this.children.add(child);
        child.setMother(this);
    }

    public void removeChild(Child child) {
        this.children.remove(child);
        child.setMother(null);
    }
}

@DataJpaTest
public class MotherTest {

    @Autowired
    private MotherRepository motherRepository;

    @Autowired
    private ChildRepository childRepository;

    @Test
    void removeChild_MotherHasChild_ChildDeleted() {
        // Given
        Mother mother = motherRepository.save(new Mother());
        Child child = childRepository.save(new Child());
        mother.addChild(child);
        motherRepository.flush();

        // When
        mother.removeChild(child);
        motherRepository.flush();

        // Then
        assertThat(mother.getChildren()).isEmpty(); // Success
        assertThat(childRepository.findAll()).isEmpty(); // Failure
    }

}

위 테스트 코드가 실패했다.
Mother가 Child로 OneToMany(cascade=REMOVE, orphanRemoval=true) 관계를 가지는데, 컬렉션에서 Child를 제거해 orphan으로 만들어도 Child가 지워지지 않는다.

원인 파악 과정

로그 확인하기

Hibernate: 
    insert 
    into
        mother
        (id) 
    values
        (null)
Hibernate: 
    insert 
    into
        child
        (id, mother_id) 
    values
        (null, ?)
2021-02-08 15:51:56.624 TRACE 936 --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [null]
Hibernate: 
    update
        child 
    set
        mother_id=? 
    where
        id=?
2021-02-08 15:51:56.629 TRACE 936 --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
2021-02-08 15:51:56.629 TRACE 936 --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [BIGINT] - [1]
Hibernate: 
    update
        child 
    set
        mother_id=? 
    where
        id=?
2021-02-08 15:51:56.632 TRACE 936 --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [null]
2021-02-08 15:51:56.633 TRACE 936 --- [    Test worker] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [BIGINT] - [1]
Hibernate: 
    select
        child0_.id as id1_0_,
        child0_.mother_id as mother_i2_0_ 
    from
        child child0_

orphanRemoval=true가 적용되었음에도 불구하고, removeChild() 시 child 테이블의 mother_id 값만 NULL로 바꿀 뿐이며 delete 쿼리가 날아가지 않는다.
Hibernate의 더 자세한 로그도 확인해봤는데, Mother가 Child와 연관 관계를 맺고 있는 것은 확인할 수 있었지만 orphan이 된 Child를 지우지 않는 이유는 찾을 수 없었다.

이전 커밋으로 돌아가기

그러고보니 분명 얼마 전에는 잘 작동했었다. 언제부터 문제가 생긴거지?
git reset --hard HEAD~{Number} 명령어를 통해 특정 커밋 전으로 돌아간 뒤 정상적으로 작동함을 확인하고 그 변경 사항을 확인했다.

프로젝트 진행 중 CascadeType을 일괄적으로 바꾼 적이 있다. 항상 CascadeType.ALL을 활용하기보다는 필요에 따라 쓰기로 했던 것이다. 저번에 발생했던 문제를 겪으며 앞으로 굳이 필요하지 않다면 CascadeType.PERSIST를 사용하지 말자는 의사결정을 내렸기 때문이다.

연관 관계의 cascade 옵션에 CascadeType.PERSIST를 포함시키면 orphanRemoval가 작동하고 아무런 문제가 없었다.
하지만 orphanRemoval과 cascade 기능의 상관관계를 도무지 이해할 수 없어서 조금 더 삽질해보기로 했다.

뭐지 버근가?

JPA 2.2 Specification 문서를 읽어도 관련된 내용은 적혀있지 않았다. 설마 Hibernate의 버그인가 싶어서 JIRA의 이슈 트래커에서 검색을 해봤다.

그러던 중 관련된 이슈를 찾았다. 과거에 해당 버그가 수정됐으나 다른 이유로 롤백된듯 하다.
자세한 사정을 보니, JPA 스펙에 의하면 OneToOne 관계에서 cascade=PERSIST를 걸어두지 않았을 때 자식이 Transient 상태이면 IllegalStateException이 던져져야 하는데, orphanRemoval 버그를 수정하면 이게 작동하지 않는다는 것이다.
여전히 orphanRemoval과 cascade 기능의 상관관계를 이해할 수는 없지만 Hibernate의 버그라고 하니 어쩔 수 없는 것 같다.
그렇다고 이 버그가 해결되기를 기다리는 건 무리인 것 같다. 해당 이슈는 2015년에 등록됐지만 6년이 지난 지금도 해결되지 않은 체로 남아있기 때문이다.

해결

    @ToString.Exclude
    @OneToMany(mappedBy = "mother", cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, orphanRemoval = true)
    private Set<Child> children = new HashSet<>();

어쩔 수 없이 orphanRemoval을 사용하는 곳에 CascadeType.PERSIST를 포함시키기로 했다. 지금까지 PERSIST를 넣지 말아야 할 경우는 부모가 둘 이상인 경우밖에 없었고, 이 때에는 orphanRemoval 기능이 필요하지 않아서 문제는 없다.
이왕 PERSIST를 넣는 김에 CascadeType.ALL을 박으면 관련된 기능들도 추가로 얻고 코드도 조금 간결해질 것 같다.

결론

orphanRemoval이 적용된 연관 관계에는 CascadeType.PERSIST를 포함시켜야 한다. 이는 Hibernate(5.4.27.Final 기준)의 버그이다.

반성

아무리 작은 변경이라도 커밋하기 전 습관적으로 테스트 코드를 돌려보자. 그렇지 않으면 언제부터 문제가 발생했는지 특정하기 어렵다..

profile
🦈

0개의 댓글