728x90
반응형
SMALL
반응형
SMALL

2024.10.30 업데이트


 

CASCADE는 영속성 전이를 어떤식으로 동작시킬 거냐를 지정하는 옵션이다.

일단 가장 먼저 알고 넘어갈 건 이 CASCADE는 연관관계나 지연로딩 즉시로딩과 전혀 아무런 상관이 없다.

 

그저 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티를 어떻게 할건지를 지정한다. 예를 들면 부모를 저장할 때 부모에 연관된 자식도 같이 영속하는 경우를 말한다.

 

말로는 잘 이해가 안되고 코드로 보면 바로 이해가 된다. 

 

Parent

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

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();
        }
    }
}
  • 그럼 이렇게 코드가 작성될거다. 근데 사실 이게 좀 귀찮은 작업이다. parentchild, 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에 부여하면 @OneToMany 애노테이션이 달린 필드의 타입인 Child들도 전이를 시키겠다는 것이다. 그러니까 CASCADE는 딱 이렇게 생각하면 좋다. cascade 옵션을 설정한 필드 타입은 (여기서는 List<Child>이니까 Child를 말함) 이 Parentpersist()로 영속시키거나 remove()로 삭제할 때 이 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 정도가 주로 사용된다. 부모를 영속시킬 때 자식들도 한번에 다 영속시키거나, 부모를 삭제하면 그 하위 자식들도 다 같이 삭제되는 그런 경우가 거의 일반적이니까.

  • ALL: 모두 적용
  • PERSIST: 영속
  • REMOVE: 삭제
  • MERGE: 병합
  • REFRESH: REFRESH
  • DETACH: 준영속 

CASCADE는 자주 사용되는 편리한 옵션이지만, 주의할 사항이 있다. 위 예시처럼 Parent - Child 딱 하나로만 이루어진 연관 엔티티가 있는 경우는 CASCADE를 적용해도 무방하다. 근데 만약, ParentChild를 가지고 있고, 다른 어떤 엔티티(예를 들면.. 유치원 선생님 엔티티?)도 Child를 가지고 있다면 CASCADE 옵션을 사용하면 안된다. 위험하다. 예를 들어 A, B 엔티티가 있고 C라는 엔티티가 있을 때, A와 B 모두 C에 대한 부모가 되는 경우에 A에 CASCADE 옵션으로 REMOVE 또는 ALL을 적용해버리면 A를 삭제하면 C도 삭제가 될 것이다. 근데 B는 이를 모르고 있는 상태가 된다. 영문도 모른채 본인의 자식 엔티티 객체가 사라져버린다. 이런 이유때문에 조심해야한다. 다음 코드가 그 예시다.

 

Parent

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

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

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와 관계를 가지고 있고, ParentParentTwo 둘 모두 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 옵션은 사용을 하면 안된다.

 

 

고아 객체 (orphan)

고아 객체라는 건 부모 - 자식 관계를 가지고 있는 엔티티 구조에서 부모 엔티티와 연결이 끊어진 자식 객체를 말한다. 

그리고 JPA에서는 이 고아 객체를 제거해버리는 옵션이 주어지는데 이 옵션이 orphanRemoval 옵션이다.

 

바로 확인해보자.

 

Parent

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 라는 옵션을 주었다. 이 옵션을 주면 이 부모와 연결이 끊어진 자식 객체는 삭제가 된다.

 

Main

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에 추가된다. 그리고 나서 이 코드를 보자.
Parent findParent = em.find(Parent.class, parent.getId());
findParent.getChildList().remove(0);
  • 엔티티 매니저로 부모 객체를 찾아서 부모 객체가 가지고 있는 자식을 remove() 했다. 그럼, 0번에 해당하는 Child 객체는 이제 더이상 부모인 Parent 객체의 리스트에서 관리되지 않는다. (여기서 혼동이 많은데, 지금 remove()를 호출한다고 한들, 당연히 DB에는 아무런 영향이 없다! 즉, 0ChildPARENT_ID 외래키가 없어지는게 아니다! 그저 객체 관점에서 0Child가 더 이상 부모 참조가 없어진 것 뿐이다. 그러나! `orphanRemoval = true` 활성화를 하면 객체가 고아가 되면 DB에도 영향을 준다는 것이다. DB에서도 삭제가 되니 말이다.) 
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문이 보인다. 이게 고아 객체를 제거하는 쿼리다.

 

그리고 이 경우에도 주의할 점은! CASCADE와 마찬가지 이유로, 반드시 참조하는 곳이 하나일 때 사용해야 한다! 즉, 특정 엔티티가 개인 소유하는 경우에만 사용해야 한다. (예: 게시글 - 첨부파일 관계) 

  

참고로, 개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 `orphanRemoval = true` 활성화하면, 부모를 제거할 때 자식도 함께 제거된다. 이것은 CASCADEREMOVE처럼 동작한다.

 

영속성 전이 + 고아 객체

만약, CascadeType.ALL + orphanRemoval = true 이 두가지 옵션을 모두 설정을 하면, 부모 엔티티를 통해서 자식의 생명 주기를 관리할 수 있게 된다. 영속성 전이를 통해 영속을 시키든, 삭제를 시키든 그 하위 엔티티도 동일하게 동작할 것이고, 부모에서 자식을 떼버리면 고아 객체가 된 객체는 DB에서 삭제되기 때문에 완전히 부모를 통해 자식의 생명 주기를 관리할 수 있게 된다.

728x90
반응형
LIST

+ Recent posts