본문 바로가기
JPA

Cascade, OrphanRemoval 사용 주의

by 근즈리얼 2022. 2. 5.
728x90

이번에 특정 기능을 개발한 후 만났던 버그에 대한 정리 및 회고 포스팅입니다~!

상황 설명 ㅠㅠ

기존에 A 라는 객체에 대해서 생성, 삭제, 수정의 기능이 있었습니다.

이때, 저는 A를 복사하는 기능을 개발했었습니다.

정말 엄청난 고생을 하면서 복사하는 기능을 만들고 테스트하고 QA팀도 통과하고 배포 후 테스트까지 성공하면서 다행이다~! 라는 생각을 하고 앞으로 행복한 날만 남았다고 생각하며(물론... 저의 첫 기능 개발이었지만 ㅎㅎ) 행복한 주말을 보내고 월요일에 출근을 했습니다.

이때.... 갑자기 A의 특정 값을 지우고 업데이트를 할 때 특정 값이 지워지지 않는 버그가 발생한다는 소식을 들었고

저는 급하게 원인을 분석하게 되었습니다. 그리고... 그 다음날 급하게 핫픽스를 진행하게 되었습니다.... ㅠㅠ

 

원인이 된 구조

 우선, 간단하게 원인이 되었던 부분을 중심으로 구조를 알아보겠습니다.

 

A안에는 A의 기능이 담겨있는 B라는 객체가 있습니다.

그리고 B라는 객체는 자식으로 B를 갖을 수 있습니다.

 

즉, A와 B는 일대다 관계이고

B와 B또한 일대다 관계가 되는 것입니다.

A a = new A();

B b1 = new B();
B b2 = new B();

a.get자식들().add(b1);
a.get자식들().add(b2);
b1.set부모(a);
b2.set부모(a);

b1.geta자식들().add(b2);
b2.setb부모(b1);

 

그리고

A와 B는 CascadeType.ALL과 OrphanRemoval.true가 걸려있었습니다.

B와 B의 관계에서는 아무런 설정이 되어있지 않았습니다.

 

따라서,

A에서 B를 조회한 뒤 컬렛션에서 제거하면 OrphanRemoval을 통해서 데이터베이스에서 잘 삭제될 수 있었습니다.

 

하지만, 저는 복사를 만들기 위해서 새로운 A와 B들을 만들어야 했고 그런 과정에서

B와 B사이에 CascadeType.ALL 과 OrphanRemoval.true 추가하게 되었습니다.

이때, 기존 코드로는 B를 지울 수 없는 상황이 발생했고

이 상황으로 인해 A의 특정 기능을 제거해서 수정하더라도 수정이 되지 않는 버그가 발생하게 된 것입니다.

 

원인 분석 및 해결 방법

처음에는 a의 자식에서 b2를 제거할 때 영속성 컨텍스트에서 제거가 되었다가

b1안에 b2가 남아있어서 다시 영속성 컨텍스트에 들어가는 건가? 라는 생각을 했었습니다. 

 

하지만, 이 생각에 많은 고민을 하게 되었고 결국 생각을 바꾸게 하는 과정이 발생했습니다.

바로! a와 b2의 관계를 유지한 채 b1과 b2를 제거했을 때 delete 쿼리가 발생하며 잘 삭제가 된 것입니다.

 

처음 생각대로라면 b2가 b1과 관계가 끊어지더라도 a와의 관계가 남아있기 때문에 영속성 컨텍스트에 다시 들어가야 한다고 생각합니다.

 

A = Parent , B = Child로 생각해주시면 됩니다!

package com.example.test.casecadetest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.transaction.Transactional;
import java.util.List;
import java.util.Optional;

@Service
@Transactional
public class SaveService {

    @Autowired
    private ChildRepository childRepository;

    @Autowired
    private ParentRepository parentRepository;

    public void test(){
        Parent parent = new Parent();

        Child child1 = new Child();
        Child child2 = new Child();

        // parent와 child1, child2의 연관관계 추가
        parent.getChildren().add(child1);
        child1.setParent(parent);

        parent.getChildren().add(child2);
        child2.setParent(parent);

        //child1 과 child2의 관계 추가
        child1.getChildren().add(child2);
        child2.setUpChild(child1);

        parentRepository.save(parent);

        // parent 에서 연관관계를 끊으면 delete 쿼리가 나가지 않음
        //parent.getChildren().remove(child2);

        // child 와의 관계를 끊으면 delete 쿼리가 나감
        child1.getChildren().remove(child2);
    }

}

delete 쿼리가 나가는 것을 볼 수 있습니다.

 

반대로 위의 주석을 없애고 밑의 문장에 주석을 주면 delete 쿼리가 발생하지 않는 것을 볼 수 있습니다.

 

이런 상황에서 책과 구글링을 통해서 사방으로 알아봤지만 뭔가... 명확한 정답을 찾지는 못했습니다.

 

다만 제가 내린 결론은 있습니다!!

CascadeType.ALL 과 OrphanRemoval.true를 동시에 사용하면 부모가 자식의 생성주기를 관리할 수 있다고 합니다.

이 문장을 중심으로 그림을 하나 보겠습니다.

- 위의 그림을 보고 내린 결론입니다.

- a가 관리하는 생명주기안에서 b2의 목숨은 2개인것입니다.

- 따라서 a와의 관계를 끊더라도 목숨이 하나 더 남아있던 것이고 delete쿼리가 발생하지 않은 것이빈다.

- 반면에 b1이 관리하는 생명주기에서는 b2의 목숨은 한개 입니다.

- 따라서 b1과의 관계를 끊게되면 b2의 목숨은 끝이 났고 delete 쿼리가 발생한 것입니다.

 

개인적으로,

b1과 b2의 관계를 끊더라도 데이터베이스에서 b2가 잘 삭제되었지만 아직 트랜잭션이 끝나지 않았다면 a안에 객체가 남아있기 때문에 주의해야합니다.

따라서, Cascade와 OrphanRemoval이 모두 사용되는 경우에는 관련된 객체의 모든 정보를 끊어줘야 한다고 생각합니다. 

 

회고

1. Cascade를 사용할 때 예상치 못한 부분에서의 문제가 생길 수 있기 때문에 주의해야 한다고 느꼈습니다.

2. Cascade 뿐만 아니라 jpa에 대한 기본이 아직 부족하고 더 착실하고 정확한 공부가 필요하다고 느꼈습니다 ㅠㅠ

3. 모든 기능에 대해서 다양한 방법으로 test를 진행할 필요가 있다고 느꼈습니당....

 

저 뿐만 아니라 이 글을 본 모든 분들이 Cascade를 사용할 때 좀 더 정확히 사용할 수 있기를 바라며 이만 물러가겠습니당~~

728x90

댓글