영속성 전이 : CASCADE
- 특정 엔티티를 영속 상태로 만들때 연관된 엔티티도 함께 영속 상태로 만들고 싶을때 사용합니다.
- eg. 부모 엔티티 저장시 자식 엔티티도 저장하게끔.
언제 쓰느냐?
- 하나의 부모가 자식들을 관리할 때 ( 단일 엔티티에 종속적인 경우)
- 라이프사이클이 같을때, 단일 소유자
쓰면 안되는 케이스?
- 자식의 연관 관계가 2개 이상일때
- 예를 들어 다음과 같은 경우에는 쓰면 안된다.
- 그림처럼 C 라는 엔티티가 A, B 에 의해서 관리된다고 하자.
- 이런 경우에서는 영속성 전이를 사용해서는 안된다.
참고
CASCADE 의 옵션은 ALL, PERSIST, REMOVE, MERGE, REFRESH, DETACH 가 있지만
주로 사용되는건 ALL, PERSIST, REMOVE 정도이고 실무에서 사용되는 건 ALL, PERSIST
각 옵션이 의미하는 것은 다음과 같다.
- ALL : 모두 적용
- PERSIST : 영속
- REMOVE : 삭제
- MERGE : 병합
- REFRESH : REFRESH
- DETACH : DETACH
PERSIST
영속한다. 쉽게 이야기하면 부모 엔티티를 저장할 때 연관된 자식 엔티티도 같이 저장합니다.
코드 예시를 하나 들어보죠.
@Entity
@Getter
@Setter
public class Parent {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
private List<Child> childList = new ArrayList<>();
public void addChild(Child child) {
this.childList.add(child);
child.setParent(this);
}
}
@Entity
@Getter
@Setter
public class Child {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
}
- 위와 같이 parent, child 가 있고 parent 가 child 를 관리할 수 있도록 cascade 옵션을 PERSIST 로 줬습니다.
- 이런 경우에 parent 엔티티가 저장될때 parent entity 에 childList 에 뭔가 새로운 값이 추가되면 이것도 같이 추가되겠죠.
Child child1 = new Child();
Child child2 = new Child();
Parent parent =new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent);
em.flush();
em.clear();
tx.commit();
- 원래 우리가 엔티티를 저장할 때 엔티티마다 persist 를 해줘야 합니다.
- 그럼 사실 코드는 아래처럼 되었어야 겠죠?
em.persist(child1);
em.persist(child2);
em.persist(parent);
- 사실 cascade 옵션이 없었다면 em.persist() 가 3개의 엔티티를 저장하기 위해서 3번 호출이 되어야 했습니다.
- 하지만 여기서 한번만 호출하면 충분하죠.
- 또한 위와 같은 코드를 실행하고 로그를 확인하면 3번의 insert 쿼리가 실행됩니다.
- 그럼 이 옵션에서 parent 삭제하면 어떻게 될까? 자식이 삭제될까?
- 느낌으로는 자식도 삭제되야 될 것 같습니다.
em.flush();
em.clear();
Parent findParent = em.find(Parent.class, parent.getId());
em.remove(findParent);
- 영속성 컨텍스트를 초기화하고 parent 를 가져와 삭제해보면 그럼 자식을 삭제하는 삭제 쿼리가 나갈까요?
- 아닙니다. 심지어 에러가 발생한다. child가 parent 를 참조하고 있기 때문이죠.
- 이를 삭제하려면 어떻게 해야될까?
REMOVE
- 부모 엔티티를 삭제할때 연관되어 있는 자식 엔티티도 같이 삭제하는 옵션입니다.
- Parent 클래스의 @OneToMany 의 cascade 값을 REMOVE 로 변경하고 실행하면 delete query 가 세 개가 나가 자식들도 삭제됩니다.
- 이 뿐만 아니라 이 상태의 전이가 하위로 계속해서 발생합니다.
ALL
REMOVE + PERSIST
정리해보자!
CASCADE 는 부모의 영속성 상태를 자식에게 전달할 때 사용한다.
부모를 저장할 때, 추가된 자식을 저장하거나 부모를 삭제할 때 연관된 자식을 삭제하는 것이다.
따라서 확실하게 자식을 관리하는 엔티티가 부모 하나일때만 사용해야 하며, 라이프 사이클이 같을 때 써야한다는 것이다.
만약에 자식 엔티티를 관리하는 또 다른 엔티티가 있는데 부모 엔티티가 삭제된다고 자식 엔티티도 삭제하면 또 다른 엔티티에서 사용하지 못하는 이슈가 발생하기 때문이다.
고아 객체
- 고아 객체라는 말이 좀 그렇긴 한데, 쉽게 이야기해서 부모 엔티티와 연관 관계가 끊어진 자식 엔티티를 말합니다.
- 옵셥으로 부모 엔티티와 연관 관계가 끊어진 자식 엔티티를 자동으로 삭제할 수 있습니다.
orphanRemoval = true
- 옵션을 주게 되면 부모 엔티티에서 자식 엔티티를 제거하면 delete 쿼리가 나갑니다. CascadeType.REMOVE 처럼 동작하죠.
- 주의해야 할 점은 반드시 참조하는 곳이 하나 ( 특정 엔티티가 개인 소유할때)일 때 사용해야 합니다.
- 그럼 여기서 CascadeType.REMOVE 랑 orphanRemoval = true 의 차이점이 뭔가에 대해서 다시 짚고 갑시다.
- CascadeType.REMOVE 는 부모 엔티티를 삭제할 때 연관된 자식 엔티티를 삭제한다.
- orphanRemoval = true 는 연결이 끊어진 자식 엔티티를 삭제한다.
- 비슷하지만 다르기 때문에 명심해야 합니다.
@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST, orphanRemoval = true)
- 옵션을 위처럼 변경하고 다시 해보자.
- CascadeType.REMOVE 와 동일하게 동작한다.
- 부모 엔티티를 삭제했다는 의미는 즉, 부모 엔티티와 연관을 맺고 있던 자식들이 고아가 됐다는 의미이다.
- 그래서 이때는 REMOVE 와 동일하게 동작하는 것이다.
- 그럼 아래처럼 해보자.
Parent findParent = em.find(Parent.class, parent.getId());
findParent.getChildList().remove(0);
- parent 의 자식 리스트에서 하나를 삭제했다. 이때 쿼리는 어떻게 될까?
- findParent 와 연결을 끊어버린 0번째 자식을 고아로 만든것이고 이는 즉 0번째 자식을 삭제하는 쿼리를 발생시킨다.
cascade = CascadeType.ALL + orphanRemoval = true
- 두 옵션을 모두 활성화하면 어떻게 될까?
- 이러면 단순히 부모 엔티티에서 자식 엔티티의 모든 생명 주기를 관리할 수 있게 된다.
- CascadeType.PERSIST + orphanRemoval = true 의 조합과 위의 조합이 뭐가 다를까 의문이 들었다.
- 사실 여기서 영속성 전이라는 개념에 대해서 잘 이해해야 한다.
- 상태를 전이한다는 것은 트리처럼 연관 관계에 속한 모든 것에 계속해서 상태를 전이한다는 것을 말한다.
- CascadeType.REMOVE 라고 해보자.
- CascadeType.REMOVE 는 명시적인 remove 메소드 호출이 일어나지 않으면 삭제가 일어나지 않는다.
- 예를 들어보자.
- parent 엔티티가 @OneToMany 로 child 엔티티와 연관 관계를 맺고 있다고 한다.
- 그럼 이 때 parent 엔티티를 가져와 child 엔티티를 명시적으로 삭제한다고 해보자. 이 때는 명시적인 remove 메소드를 호출했다.
- 이 때는 데이터베이스에서 child 엔티티에 대한 삭제가 발생한다.
- 하지만 단순히 parent.getchildList().clear() 로 연결을 끊어버린다고 해보자. 이 때는 데이터베이스에서는 직접 삭제가 일어나지 않는다.
- 고아 객체를 만들어버리겠다는 의지를 보이는 코드임에도 불구함에도 말이다.
- 하지만 orphanRemoval = true 의 경우 객체의 레퍼런스를 확인하기 때문에 REMOVE 와는 다르게 데이터베이스 삭제가 일어난다.
정리
이 부분에서 가장 헤깔릴 수 있는 부분이 CascadeType.REMOVE 와 orphanRemoval = true 가 아닐까 싶다.
부모 엔티티가 삭제되면 자식 엔티티도 삭제된다 => REMOVE
부모 엔티티에서 자식만 삭제하고 싶다 => orphanRemoval = true
정도로 이해하고 넘어가자.
연관 관계 관리
- 모든 연관 관계는 지연 로딩으로 사용한다. OneToOne ManyToOne 은 기본 전략이 EAGER 임으로 꼭 LAZY 로 설정해줘야 한다.
- 라이프사이클을 같이 관리하고 싶다면 영속성 전이 옵션을 CascadeType.ALL 로 지정한다(부모 엔티티와 자식 엔티티간 1:1 관계일 경우)
참고 : https://velog.io/@devsh/JPA-CASCADE-%EC%98%81%EC%86%8D%EC%84%B1-%EC%A0%84%EC%9D%B4
'JPA' 카테고리의 다른 글
(로그 관련) p6spy 설정 방법 (0) | 2023.06.22 |
---|---|
Entity Manager Factory와 persist, flush, commit (0) | 2023.03.06 |
SpringBoot JPA Multiple Databases 설정 (0) | 2022.10.02 |
Java8 Optional 의 ifPresent 활용 (2) | 2022.09.01 |
@Transactional(rollbackFor = Exception.class) 에 대해 (0) | 2022.08.26 |