CASCADE는 영속성 전이를 어떤식으로 동작시킬 거냐를 지정하는 옵션이다.
일단 가장 먼저 말할거는 이 CASCADE는 연관관계나 지연로딩 즉시로딩과 전혀 아무런 상관이 없다.
그저 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티를 어떻게 할건지를 지정한다. 예를 들면 부모를 저장할 때 부모에 연관된 자식도 같이 저장하는 경우를 말한다. 그러니까 CASCADE는 부모 엔티티에서만 설정한다고 봐도 사실 무방하다고 본다.
말로는 잘 이해가 안되고 코드로 보면 바로 이해가 된다.
Parent Class
package org.example.entity.cascade;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
public class Parent {
@Id @GeneratedValue
@Column(name = "PARENT_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "parent")
private List<Child> childList = new ArrayList<>();
public void addChild(Child child) {
this.getChildList().add(child);
child.setParent(this);
}
}
Child Class
package org.example.entity.cascade;
import javax.persistence.*;
@Entity
public class Child {
@Id @GeneratedValue
@Column(name = "CHILD_ID")
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "PARENT_ID")
private Parent parent;
}
이렇게 두 엔티티가 있다고 가정했을 때, Parent가 2개의 Child를 가지는 코드를 작성한다고 해보자.
package org.example;
import org.example.entity.Team;
import org.example.entity.cascade.Child;
import org.example.entity.cascade.Parent;
import org.example.entity.fetch.Member;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import java.util.List;
public class Main {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
// ! Transaction 시작
tx.begin();
try {
Parent parent = new Parent();
parent.setName("parent");
Child child = new Child();
child.setName("childA");
Child childB = new Child();
childB.setName("childB");
parent.addChild(child);
parent.addChild(childB);
em.persist(parent);
em.persist(child);
em.persist(childB);
tx.commit();
} catch (Exception e) {
e.printStackTrace();
tx.rollback();
} finally {
em.close();
emf.close();
}
}
}
그럼 이렇게 코드가 작성될거다. 근데 사실 이게 좀 귀찮은 작업이다. parent에 child, childB가 속해지는데 parent, child, childB 모두 영속시키는 코드를 작성해줘야 하니까. child, childB가 어차피 parent에 속한다면 그냥 parent 하나를 영속시켜서 한번에 모두 다 영속시키면 더 편리하지 않을까? 그게 가능하다. 다음 코드를 보자.
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List<Child> childList = new ArrayList<>();
Parent가 가지고 있는 childList 필드에 @OneToMany로 연관관계를 걸어놓은 상태에서 CascadeType.ALL 속성을 CASCADE에 부여하면 Parent에 달려있는 다른 엔티티(Child)의 객체를 같이 영속시키겠다는 의미다.
이렇게 변경하고 실행 코드에서 child들을 persist() 메소드로 호출하는 라인을 지워보자.
package org.example;
import org.example.entity.Team;
import org.example.entity.cascade.Child;
import org.example.entity.cascade.Parent;
import org.example.entity.fetch.Member;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import java.util.List;
public class Main {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
// ! Transaction 시작
tx.begin();
try {
Parent parent = new Parent();
parent.setName("parent");
Child child = new Child();
child.setName("childA");
Child childB = new Child();
childB.setName("childB");
parent.addChild(child);
parent.addChild(childB);
em.persist(parent);
//em.persist(child);
//em.persist(childB);
tx.commit();
} catch (Exception e) {
e.printStackTrace();
tx.rollback();
} finally {
em.close();
emf.close();
}
}
}
그래도 역시 실행했을 때 INSERT문으로 parent, child, childB가 모두 들어간다.
CASCADE는 이게 끝이다. 연관관계나 프록시와는 아무런 상관이 없다.
CASCADE 종류
CASCADE의 종류는 다음과 같다.
- ALL: 모두 적용
- PERSIST: 영속
- REMOVE: 삭제
- MERGE: 병합
- REFRESH
- DETACH: 준영속
CASCADE는 자주 사용되는 편리한 옵션이지만, 주의할 사항이 있다. 위 예시처럼 Parent - Child 딱 하나로만 이루어진 연관 엔티티가 있는 경우는 CASCADE를 적용해도 무방하다. 근데 만약 Parent도 Child를 가지고 다른 어떤 엔티티도 Child를 가지고 있다면 CASCADE 옵션을 사용하면 안된다. 위험하다. 예를 들어 A, B 엔티티가 있고 C라는 엔티티가 있을 때, A와 B 모두 C에 대한 부모가 되는 경우에 A에 CASCADE 옵션으로 REMOVE 또는 ALL을 적용해버리면 A를 삭제하면 C도 삭제가 될 것이다. 근데 B는 이를 모르고 있는 상태가 된다. 영문도 모른채 본인의 자식 엔티티 객체가 사라져버린다. 이런 이유때문에 조심해야한다. 다음 코드가 그 예시다.
Parent Class
package org.example.entity.cascade;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
public class Parent {
@Id @GeneratedValue
@Column(name = "PARENT_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List<Child> childList = new ArrayList<>();
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Child> getChildList() {
return childList;
}
public void setChildList(List<Child> childList) {
this.childList = childList;
}
public void addChild(Child child) {
this.getChildList().add(child);
child.setParent(this);
}
}
ParentTwo Class
package org.example.entity.cascade;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
public class ParentTwo {
@Id @GeneratedValue
@Column(name = "PARENTTWO_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "parentTwo", cascade = CascadeType.ALL)
private List<Child> childList = new ArrayList<>();
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Child> getChildList() {
return childList;
}
public void setChildList(List<Child> childList) {
this.childList = childList;
}
public void addChild(Child child) {
this.childList.add(child);
child.setParentTwo(this);
}
}
Child Class
package org.example.entity.cascade;
import javax.persistence.*;
@Entity
public class Child {
@Id @GeneratedValue
@Column(name = "CHILD_ID")
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "PARENT_ID")
private Parent parent;
@ManyToOne
@JoinColumn(name = "PARENTTWO_ID")
private ParentTwo parentTwo;
public ParentTwo getParentTwo() {
return parentTwo;
}
public void setParentTwo(ParentTwo parentTwo) {
this.parentTwo = parentTwo;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Parent getParent() {
return parent;
}
public void setParent(Parent parent) {
this.parent = parent;
}
}
주의 깊게 볼 내용은 Parent, ParentTwo 클래스 모두 Child와 일대다 관계를 가지고 있고, Parent와 ParentTwo 둘 모두 CASCADE옵션을 ALL로 지정했다는 것이다. 여기서 만약, Parent 또는 ParentTwo 둘 모두에게 연결된 Child가 있는 Parent 또는 ParentTwo 레코드가 있고 그 레코드를 삭제하면 어떻게 될까? Child도 삭제가 된다. 왜냐하면 CASCADE옵션을 ALL로 주었기 때문에 PERSIST, REMOVE, DETACH등 모든 옵션이 적용된다. 그럼 다른 부모 입장에서 갑자기 자식이 사라진 셈이다. 망한거다.
package org.example;
import org.example.entity.Team;
import org.example.entity.cascade.Child;
import org.example.entity.cascade.Parent;
import org.example.entity.cascade.ParentTwo;
import org.example.entity.fetch.Member;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import java.util.List;
public class Main {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
// ! Transaction 시작
tx.begin();
try {
Parent parent = new Parent();
parent.setName("parent");
ParentTwo parentTwo = new ParentTwo();
parentTwo.setName("parentTwo");
Child child = new Child();
child.setName("childA");
parent.addChild(child);
parentTwo.addChild(child);
em.persist(parent);
em.persist(parentTwo);
em.flush();
em.clear();
ParentTwo parentTwo1 = em.find(ParentTwo.class, parentTwo.getId());
em.remove(parentTwo1);
tx.commit();
} catch (Exception e) {
e.printStackTrace();
tx.rollback();
} finally {
em.close();
emf.close();
}
}
}
주의 깊게 볼 부분은 flush가 된 후 다시 찾아낸 ParentTwo 객체를 지우는 이 부분이다.
ParentTwo parentTwo1 = em.find(ParentTwo.class, parentTwo.getId());
em.remove(parentTwo1);
이러면 Child가 날아가버린다. 확인해보자.
Hibernate:
delete
from
Child
where
CHILD_ID=?
Hibernate:
delete
from
ParentTwo
where
PARENTTWO_ID=?
이러한 상황을 조심해야 하기 때문에 딱 관계가 Parent - Child처럼 다른 참조 객체가 없는 상황에서는 상관이 없지만 그게 아닌 경우 CASCADE 옵션은 사용을 지양해야 한다.
고아 객체
고아 객체라는 건 부모 - 자식 관계를 가지고 있는 엔티티 구조에서 부모 엔티티와 연결이 끊어진 자식 객체를 말한다.
그리고 JPA에서는 이 고아 객체를 제거해버리는 옵션이 주어지는데 이 옵션이 orphanRemoval 옵션이다.
바로 확인해보자.
Parent Class
package org.example.entity.cascade;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
public class Parent {
@Id @GeneratedValue
@Column(name = "PARENT_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Child> getChildList() {
return childList;
}
public void setChildList(List<Child> childList) {
this.childList = childList;
}
public void addChild(Child child) {
this.getChildList().add(child);
child.setParent(this);
}
}
이 부모 클래스에서 볼 부분은 다음 코드다.
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
보면 orphanRemoval = true 라는 옵션을 주었다. 이 옵션을 주면 이 부모와 연결이 끊어진 자식 객체는 삭제가 된다.
확인해보자.
실행 코드
package org.example;
import org.example.entity.Team;
import org.example.entity.cascade.Child;
import org.example.entity.cascade.Parent;
import org.example.entity.cascade.ParentTwo;
import org.example.entity.fetch.Member;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import java.util.List;
public class Main {
public static void main(String[] args) {
EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
// ! Transaction 시작
tx.begin();
try {
Parent parent = new Parent();
parent.setName("parent");
Child child = new Child();
child.setName("child");
parent.addChild(child);
em.persist(parent);
em.flush();
em.clear();
Parent findParent = em.find(Parent.class, parent.getId());
findParent.getChildList().remove(0);
tx.commit();
} catch (Exception e) {
e.printStackTrace();
tx.rollback();
} finally {
em.close();
emf.close();
}
}
}
위 코드에서 자식을 생성하고 부모에 연결한 후 부모를 영속시킨 후 플러시를 실행했다. 여기까지 진행되면 Parent, Child 둘 모두 DB에 추가된다. 그리고 나서 엔티티 매니저로 부모 객체를 찾아서 부모 객체가 가지고 있는 자식을 remove() 했다. 이 때 일어나는 일이 부모가 사라진 자식 객체를 DB에서 지우는 것이다. 실행해보면 다음과 같은 SQL문이 출력된다.
Hibernate:
insert
into
Parent
(name, PARENT_ID)
values
(?, ?)
Hibernate:
insert
into
Child
(name, PARENT_ID, PARENTTWO_ID, CHILD_ID)
values
(?, ?, ?, ?)
Hibernate:
select
parent0_.PARENT_ID as parent_i1_2_0_,
parent0_.name as name2_2_0_
from
Parent parent0_
where
parent0_.PARENT_ID=?
Hibernate:
select
childlist0_.PARENT_ID as parent_i3_0_0_,
childlist0_.CHILD_ID as child_id1_0_0_,
childlist0_.CHILD_ID as child_id1_0_1_,
childlist0_.name as name2_0_1_,
childlist0_.PARENT_ID as parent_i3_0_1_,
childlist0_.PARENTTWO_ID as parenttw4_0_1_,
parenttwo1_.PARENTTWO_ID as parenttw1_3_2_,
parenttwo1_.name as name2_3_2_
from
Child childlist0_
left outer join
ParentTwo parenttwo1_
on childlist0_.PARENTTWO_ID=parenttwo1_.PARENTTWO_ID
where
childlist0_.PARENT_ID=?
Hibernate:
delete
from
Child
where
CHILD_ID=?
위에서부터 차례로 부모와 자식이 INSERT된 후 부모를 조회하는데 부모를 조회한 후 자식을 호출하는 과정에서 자식을 찾아내는 조인쿼리가 실행된다. (왜냐? OneToMany는 기본이 지연로딩(LAZY)이고 아무것도 설정하지 않았기 때문에 초기화하는 시점에 자식을 찾아온다) 그리고 나서 실행된 DELETE문이 보인다. 이게 고아 객체를 제거하는 쿼리다.
(물론, 이 orphanRemoval 옵션도 부모가 Parent, ParentTwo 이렇게 여러 부모가 있는 경우 한 쪽에서 자식을 지웠다고 해도 다른 한 쪽이 아직 부모 - 자식 관계를 가지고 있으면 자식 객체를 제거 하지 않는다.)
'JPA(Java Persistence API)' 카테고리의 다른 글
[JPA] Part 14. 컬렉션 값 타입 (0) | 2023.10.25 |
---|---|
[JPA] Part 13. 임베디드 타입 (0) | 2023.10.23 |
[JPA] Part 11. 지연로딩과 즉시로딩 (2) | 2023.10.23 |
[JPA] Part 10. 프록시 (0) | 2023.10.22 |
[JPA] Part 9. @MappedSuperclass (0) | 2023.10.22 |