Lined Notebook

[Spring Data JPA] 23. JPA 영속성전이와 CASECADE 옵션

by ymkim

1. 영속성 전이 : CASCADE

2022-02-22_영속성전이01

영속성 전이 CASCADE는 앞에서 설명한 즉시 로딩, 지연 로딩, 연관관계 설정과는 전혀 상관이 없는 부분이다.

영속성 전이는 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속상태로 만들고 싶을 때 사용이 된다. 대표적인 예로는 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장하는 경우를 들 수 있다.

1-1. 영속성 전이를 이해하기 위한 기본 예제

@Entity
public class Parent {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @OneToMany(mappedBy = "parent")
    private List<Child> childList = new ArrayList<>();

    // 연관관계 편의 메서드
    public void addChild(Child child) {
        childList.add(child); // 부모에 자식 셋팅
        child.setParent(this); // 자식에 부모 셋팅
    }

    //.. Getter, Setter 중략
}
  1. childList를 갖는 Parent 엔티티를 생성한다.
@Entity
public class Child {

    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Parent parent;

    //.. Getter, Setter 중략

    public Parent getParent() {
        return parent;
    }

    public void setParent(Parent parent) {
        this.parent = parent;
    }
}
  1. Parent 객체를 갖는 Child 엔티티 생성한다.
public static void main(String[] args) {
    EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
    EntityManager em = emf.createEntityManager();

    EntityTransaction tx = em.getTransaction();
    tx.begin();

    try {
        Child child1 = new Child();
        Child child2 = new Child();

        Parent parent = new Parent();
        parent.addChild(child1); // 편의 메서드에 셋팅
        parent.addChild(child2);

        em.persist(parent);
        em.persist(child1);
        em.persist(child2);

        tx.commit();
    } catch (Exception e) {
        tx.rollback();
        e.printStackTrace();
    } finally {
        em.close();
    }

    emf.close();
}

위 호출부를 보면 Child 객체를 생성한 후에 Parent 객체를 생성한다. 후에 Parent 객체에 있는 연관관계 편의 메서드를 통해 Child 객체를 각각 셋팅 해주는 상황이다.

1-2. 출력 결과

Hibernate:
    /* insert com.hello.jpatest.Parent
        */ insert
        into
            Parent
            (name, id)
        values
            (?, ?)
Hibernate:
    /* insert com.hello.jpatest.Child
        */ insert
        into
            Child
            (name, parent_id, id)
        values
            (?, ?, ?)
Hibernate:
    /* insert com.hello.jpatest.Child
        */ insert
        into
            Child
            (name, parent_id, id)
        values
            (?, ?, ?)

예상한대로 Parent와 Child의 INSERT 쿼리가 날아가는 것을 확인할 수 있다. 하지만 여기서 Parent를 조작하는 것만으로도 같은 효과를 낼 수 있는 방법이 존재하는데 이 부분이 바로 영속성 전이 CASCADE다.

1-3. CASCADE 적용

@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List<Child> childList = new ArrayList<>();

위와 같이 cascade 속성을 CascadeType.ALL로 설정해준다.

Child child1 = new Child();
Child child2 = new Child();

Parent parent = new Parent();
parent.addChild(child1); // 편의 메서드에 셋팅
parent.addChild(child2);

em.persist(parent); // 💡 이 부분 만으로도 세개의 쿼리가 날라간다.
// em.persist(child1); 제거
// em.persist(child2); 제거

기존에 사용되었던 em.persist(child1)을 제거한다.

1-4. 출력 결과

Hibernate:
    /* insert com.hello.jpatest.Parent
        */ insert
        into
            Parent
            (name, id)
        values
            (?, ?)
Hibernate:
    /* insert com.hello.jpatest.Child
        */ insert
        into
            Child
            (name, parent_id, id)
        values
            (?, ?, ?)
Hibernate:
    /* insert com.hello.jpatest.Child
        */ insert
        into
            Child
            (name, parent_id, id)
        values
            (?, ?, ?)

영속성 컨텍스트에 Child 객체를 등록(persist)하지 않았는데도 위와 같이 Parent와 Child에 INSERT 쿼리가 날라가는 것을 확인할 수 있다.

2. 영속성 전이 : 저장

2022-02-22_영속성전이_저장

@OneToMany(mappedBy = "parent", cascade  = CascadeType.PERSIST)

영속성 전이는 부모를 저장할 때 해당 부모와 연관된 관계를 갖는 엔티티를 모두 저장(persist)한다는 의미를 가지고 있다.

2-1. 영속성 전이와 연관관계 매핑의 상관관계

영속성 전이는 연관관계 매핑과는 아무 관련이 없다. 즉, 엔티티를 영속화 할 때 연관된 엔티티도 함께 영속화 한다는 편리함을 제공할 뿐이다.

2-2. CASCADE 종류

아래 CASCADE의 종류 중에서 사용하는 빈도가 높은 옵션은 ALLPERSIST다.

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

2-3. 영속성 전이를 언제 써야 하는가?

소유자가 하나인 경우만 영속성 전이를 사용하자. 즉, 단일 엔티티에 완전히 종속적인 경우에는 라이프 사이클이 동일하게 적용되기 때문에 영속성 전이를 사용해도 무방하다.

 

영속성 전이의 경우 부모 엔티티(PK 위치)와 자식 엔티티(FK 위치)가 밀접한 연관관계로 이루어져 있는 경우에만 사용해야한다. 만약 해당 자식의 소유자(부모 엔티티)가 하나 이상인 경우에는 영속성 전이 사용을 피해야한다.

3. 고아 객체

고아 객체란 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 개념이다.

3-1 옵션

@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> childList = new ArrayList<>();

orphanRemoval 속성 값을 true로 설정한다.

3-2 컬렉션에서 자식 엔티티 제거

Parent parent1 = em.find(Parent.class, id);
parent1.getChildren().remove(0); // 자식 엔티티를 제거

위 코드와 같이 컬렉션에서 연관관계 설정이 제거되는 경우 JPA에서는 DELETE 쿼리가 나간다.

3-3 출력 결과

Hibernate:
    /* delete com.hello.jpatest.Child */ delete
        from
            Child
        where
            id=?

orphanRemoval 옵션을 사용하였기에 DELETE 쿼리가 나가는 것을 확인할 수 있다.

3-4 고아 객체 사용 시 주의점

  • 참조하는 곳이 하나일 때만 사용해야한다.
  • 특정 엔티티가 개인 소유할 때만 사용해야한다.
  • @OneToOne, @OneToMany만 사용 가능.

참고 : 개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고아 객체 제거 기능을 활성화 하면, 부모를 제거할 때 자식도 함께 제거된다. 이것은 CascadeType.REMOVE 처럼 동작한다.

Parent findParent = em.find(Parent.class, parent.getId());
em.remove(findParent);

영속성 전이 + 고아 객체, 생명주기

  • 스스로 생명주기를 관리하는 엔티티는 em.persist()로 영속화, em.remove()로 제거.
// cascade와 orphanRemovel을 같이 사용하는 경우
CascadeType.ALL + orphanRemovel = true
  • 두 옵션을 모두 사용하면
  • 부모 엔티티를 통해 자식의 생명주기를 관리할 수 있다.
  • 도메인 주도 설계(DDD)의 Aggregate Root 개념 구현 시 유용한 방법이다.

참고 자료

블로그의 정보

기록하고, 복기하고

ymkim

활동하기