이번에 특정 기능을 개발한 후 만났던 버그에 대한 정리 및 회고 포스팅입니다~!
상황 설명 ㅠㅠ
기존에 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를 사용할 때 좀 더 정확히 사용할 수 있기를 바라며 이만 물러가겠습니당~~
'JPA' 카테고리의 다른 글
트랜잭션 예외처리 롤백 (0) | 2024.04.01 |
---|---|
JPA 엔티티 매핑 - 필드와 컬럼 매핑 (0) | 2021.11.23 |
JPA 엔티티 매핑 - 데이터베이스 스키마 자동 생성 (0) | 2021.11.16 |
JPA 엔티티 매핑 - 소개 및 객체와 테이블 매핑 (0) | 2021.11.15 |
jpa - 플러시, 준영속 상태 (0) | 2021.10.16 |
댓글