JPA(Java Persistence API)

[JPA] Part 12. CASCADE

cwchoiit 2023. 10. 23. 14:54
728x90
반응형
SMALL
728x90
반응형
SMALL

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 이렇게 여러 부모가 있는 경우 한 쪽에서 자식을 지웠다고 해도 다른 한 쪽이 아직 부모 - 자식 관계를 가지고 있으면 자식 객체를 제거 하지 않는다.)

  

728x90
반응형
LIST