[JPA] 영속성 전이 CASCADE, OrphanRemoval = true 이해 및 차이

2024. 11. 19. 10:52·JPA

기술 스택

  • Spring Boot 3.3.5
  • JDK 17
  • H2 Database
  • JPA

 

개요

JPA를 공부하다가 CascadeType.REMOVE와 orphanRemoval = true의 차이점에 대해 헷갈렸다. 둘다 자식 객체의 생명주기를 관리하는 코드이기 때문이였다. 그래서 테스트를 통해 알아보았다.

 

 

엔티티 기본 세팅

먼저 Parent와 Child 클래스를 생성했다. Parent는 @OneToMany, Child는 @ManyToOne으로 양방향 매핑 했다.

// Parent.java
@Entity
@NoArgsConstructor
@Getter
@Setter
public class Parent {

    @Id @GeneratedValue
    @Column(name = "PARENT_ID")
    private Long id;
    private String name;

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

    // 연관관계 편의 메서드
    public void addChild(Child child) {
        children.add(child);
        child.setParent(this);
    }
}

// Child.java
@Entity
@Getter
@Setter
@NoArgsConstructor
public class Child {

    @Id @GeneratedValue
    private Long id;

    private String name;

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

 

 

CASCADE(CASCADE.PERSIST, CASCADE.REMOVE)

영속성 전이

JPA에서 CASCADE는 엔티티 간의 관계에서 부모 엔티티의 작업을 자식 엔티티에도 자동으로 전이시키는 기능을 제공한다. 예를 들어 부모 엔티티를 저장하거나 삭제할 때 자식 엔티티도 자동으로 저장하거나 삭제하도록 설정할 수 있다.

 

CascadeType.PERSIST

  • 부모 엔티티가 영속화될 때(persist 호출), 관련된 자식 엔티티도 자동으로 영속화된다.
  • 즉, 부모 엔티티를 저장할 때 자식 엔티티들도 자동으로 함께 저장된다.

CascadeType.REMOVE

  • 부모 엔티티가 삭제될 때(remove 호출), 관련된 자식 엔티티도 자동으로 삭제된다.
  • 부모와 자식 간의 관계가 강하게 묶여 있어서 부모가 사라질 때 자식도 함께 삭제해야 하는 경우 사용된다.

CascadeType.ALL

  • 위에서 언급한 모든 CascadeType을 포함하는 옵션으로, 모든 작업(PERSIST, MERGE, REMOVE, REFRESH, DETACH)이 자식 엔티티에도 전이된다.

 

 

1. cascade = CascadeType.REMOVE, orphanRemoval = false

먼저 Parent에 casacade 속성을 'ALL', orphanRemoval는 false로 영속성 전이 옵션을 추가했다.

orphanRemoval는 default가 false이므로 생략해도 무방하다. 

Parent.java

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

cascade를 ALL을 주면 부모가 자식의 생명주기를 관리한다. 

하지만 이옵션의 경우 부모 엔티티가 자식 엔테테와의 관계를 제거해도 자식 엔티티는 삭제되지 않고 그대로 남아있다.

 

먼저 부모 엔티티를 삭제하는 경우를 살펴보겠다.

    @Test
    @DisplayName("cascade = CascadeType.ALL / orphanRemoval false: 부모 삭제하면 자식도 같이 삭제 된다.")
    void test_0(){
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        try {
            //given
            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();
            Parent findParent = em.find(Parent.class, parent.getId());

            //when
            em.remove(findParent);

            //then
            Assertions.assertThat(em.find(Child.class, child1.getId())).isNull();
            Assertions.assertThat(em.find(Child.class, child2.getId())).isNull();

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
            throw new RuntimeException(e);
        }finally {
            em.close();
        }
    }

 

DELETE 쿼리가 총 세번 나가는 것을 확인할 수 있다. 즉, 부모(Parent) 객체가 삭제될 때 자식(Child)도 영속성 전이로 인해 같이 삭제되는 것을 볼 수 있다.

Hibernate: 
    /* delete for jpa.practice.v2.Child */
    delete 
    from
        child 
    where
        id=?
Hibernate: 
    /* delete for jpa.practice.v2.Child */
    delete 
    from
        child 
    where
        id=?
Hibernate: 
    /* delete for jpa.practice.v2.Parent */
    delete 
    from
        parent 
    where
        parent_id=?

 

다음으로 부모 엔티티에서 자식 엔티티를 제거하는 경우를 알아보자.

@Test
    @DisplayName("cascade = CascadeType.ALL / orphanRemoval false: 자식을 제거해도 DELETE 쿼리가 실행되지 않는다. ")
    void test_1(){
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        try {
            //given
            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();

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

            //when
            findParent.getChildren().remove(0);
            em.flush();
            em.clear();

            //then
            Parent afterFindParent = em.find(Parent.class, parent.getId());
            Assertions.assertThat(afterFindParent.getChildren()).hasSize(2);

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
            throw new RuntimeException(e);
        }finally {
            em.close();
        }
    }

SELECT로 조회만 할 뿐 DELETE 쿼리가 전혀 나가지 않는다. cascade.REMOVE 옵션은 부모와 자식의 관계가 끊어졌다고 해서 자식을 삭제하지 않기 때문이다. 

Hibernate: 
    select
        c1_0.parent_id,
        c1_0.id,
        c1_0.name 
    from
        child c1_0 
    where
        c1_0.parent_id=?

 

 

2. cascade = CascadeType.PERSIST, orphanRemoval = true

orphanRemoval = true 옵션 또한 부모 엔티티가 삭제되면 자식 엔티티도 삭제된다. 따라서 cascadeType.PERSIST를 함께 사용하면 부모가 자식의 전체 생명주기를 관리한다. 

 

이번에는 Parent 엔티티에 고아 객체 옵션을 추가한다.

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

 

이전과 동일하게 부모 엔티티를 삭제하는 경우를 살펴보자.

    @Test
    @DisplayName("cascade = CascadeType.PERSIST / orphanRemoval true: 부모 삭제하면 자식도 같이 삭제 된다.")
    void test_2(){
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        try {
            //given
            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();
            Parent findParent = em.find(Parent.class, parent.getId());

            //when
            em.remove(findParent);

            //then
            Assertions.assertThat(em.find(Parent.class, findParent.getId())).isNull();
            Assertions.assertThat(em.find(Child.class, child1.getId())).isNull();
            Assertions.assertThat(em.find(Child.class, child2.getId())).isNull();

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
            throw new RuntimeException(e);
        }finally {
            em.close();
        }
    }

이때도 DELETE 쿼리가 세번 나가는 것을 확인할 수 있다. 즉, 부모가 삭제될 때 자식도 같이 삭제된다. 

Hibernate: 
    /* delete for jpa.practice.v2.Child */
    delete 
    from
        child 
    where
        id=?
Hibernate: 
    /* delete for jpa.practice.v2.Child */
    delete 
    from
        child 
    where
        id=?
Hibernate: 
    /* delete for jpa.practice.v2.Parent */
    delete 
    from
        parent 
    where
        parent_id=?

 

그럼 부모 엔티티에서 자식 엔티티를 제거하면 어떻게 될까? 

    @Test
    @DisplayName("cascade = CascadeType.PERSIST / orphanRemoval true: 자식을 제거하면 DELETE 쿼리가 나간다 .")
    void test_3(){
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        try {
            //given
            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();
            Parent findParent = em.find(Parent.class, parent.getId());

            //when
            findParent.getChildren().remove(0);
            em.flush();
            em.clear();

            //then
            Parent afteerFindParent = em.find(Parent.class, parent.getId());
            Assertions.assertThat(afteerFindParent.getChildren()).hasSize(1);

            tx.commit();
        } catch (Exception e) {
            tx.rollback();
            throw new RuntimeException(e);
        }finally {
            em.close();
        }
    }
}

 

이전과는 다르게 DELETE 쿼리가 한번만 나간다. 고아 객체 옵션은 부모와 자식의 관계가 끊어지면 자식을 삭제하기 때문이다. 

Hibernate: 
    /* delete for jpa.practice.v2.Child */
    delete 
    from
        child 
    where
        id=?

 

 

정리

  • CascadeType.REMOVE / orphanRemoval = false
    • 부모 엔티티에서 자식 엔티티를 삭제해도 DELETE 쿼리가 실행되지 않는다.
  • CascadeType.PERSIST / orphanRemoval = true
    • 부모 엔티티에서 자식 엔티티를 삭제하면 자식 엔티티 DELETE 쿼리가 실행된다. 

 

주의점

고아 객체 속성은 참조가 제거된 엔티니는 다른곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능이다.

즉, 참조하는 곳이 하나일 때 사용해야 한다. 이말은 즉슨 특정 엔티티가 고유로 개인 소유 할 때 사용해야한다. 

왜냐하면 자식 엔티티를 삭제할 상황이 아닌데도 참조가 제거됐다고 해서 자식 엔티티를 삭제되는 사고가 발생할 수 있기 때문이다. 

'JPA' 카테고리의 다른 글

[JPA] JPA의 타입 (임베디드 타입, 값 타입, 컬렉션 타입)이란?  (1) 2024.12.20
[JPA] 프록시와 지연로딩, 즉시로딩  (0) 2024.11.26
[JPA] 연관관계 매핑  (0) 2024.11.25
[JPA] 엔티티 매핑  (0) 2024.11.25
[JPA] JPA와 영속성 컨텍스트란?  (1) 2024.11.18
'JPA' 카테고리의 다른 글
  • [JPA] 프록시와 지연로딩, 즉시로딩
  • [JPA] 연관관계 매핑
  • [JPA] 엔티티 매핑
  • [JPA] JPA와 영속성 컨텍스트란?
Economy98
Economy98
공부하고 기록하기
  • Economy98
    Economy_Dev
    Economy98
  • 전체
    오늘
    어제
    • 분류 전체보기 (77)
      • Spring Framework (12)
      • BOJ, Programmers (22)
      • Java (6)
      • JDBC (6)
      • JPA (9)
      • Spring Transaction (3)
      • Algorithm (1)
      • Web (5)
      • Projects (2)
        • 쇼핑몰 프로젝트 (0)
        • 열람실 & 도서관 프로젝트 (2)
      • Network (2)
      • 나의 공부방 (5)
      • 끄적끄적 (1)
      • Error Log (3)
      • CS (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
    • Github
  • 링크

    • Github
  • 공지사항

  • 인기 글

  • 태그

    백준
    트랜잭션
    백준 풀이
    정렬
    restful api
    그리디 알고리즘
    스프링
    java
    백준 자바 풀이
    propagation
    자바 문제 풀이
    JPA
    스프링부트
    jdbc
    Spring
    다이나믹 프로그래밍
    예외 처리
    자바
    브루트포스 알고리즘
    자바 문제
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.1
Economy98
[JPA] 영속성 전이 CASCADE, OrphanRemoval = true 이해 및 차이
상단으로

티스토리툴바