JPA

[JPA] 영속성 전이(Cascade)와 고아 객체(orphanRemovel)

이덩우 2024. 1. 29. 17:41

1. 영속성 전이(Cascade)

영속성 전이란 연관관계를 가진 엔티티들이 있다고 했을 때 특정 엔티티를 영속시킬 때 관련된 엔티티를 모두 영속시키는 기능을 말한다.

예시를 위해 `Parent` 클래스와 `Child` 클래스를 작성해보겠다.

@Entity
public class Parent {
    @Id @GeneratedValue
    @Column(name = "parent_id")
    private Long id;
    private String name;

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

    public void addChild(Child child) {
        children.add(child);
        child.setParent(this);
    }
    .
    .
    .
}

@Entity
public class Child {
    @Id @GeneratedValue
    @Column(name = "child_id")
    private Long id;

    private String name;

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

각 엔티티를 생성하고 영속하고싶다면 아래와 같이 할 수 있을 것이다.

 

public class Main {
    public static void main(String[] args) {
        Parent parent = new Parent();
        Child child1 = new Child();
        Child child2 = new Child();

        parent.addChild(child1);
        parent.addChild(child2);

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

뭔가 귀찮다.

"parent를 영속시킬 때 모든 자식들을 한번에 영속시킬 수 없을까?" 라는 개념에서 등장한 것이 영속성 전이, cascade 속성이다.

실제로 cascade 속성을 적용해보자.

@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> children = new ArrayList<>();

    public void addChild(Child child) {
        children.add(child);
        child.setParent(this);
    }
    .
    .
    .
}

cascade 속성의 대표적인 종류는 아래와 같다.

  • `ALL` : 모든 상황에서 적용
  • `PERSIST` : 영속하는 상황에 적용
  • `REMOVE` : 삭제하는 상황에 적용

편리해보이는 기능이지만 *조심히 사용해야한다.*

  • 특정 부모 엔티티가 자식 엔티티를 *개인 소유*할 때 사용해야한다. 
    • 가령 게시물 내 댓글이라던지, 첨부파일 내 파일같은 경우이다.
    • 자식 엔티티가 다른 곳에도 부모를 두고있다면 영속성 전이가 발생하며 예상치 못한 결과가 발생한다.
  • 부모 엔티티와 자식 엔티티의 라이프 사이클이 비슷한 경우 사용해야한다.

 


2. 고아 객체(orphanRemovel)

고아 객체란 N : 1 매핑관계에서 일(1)에 속하는 *부모 객체에서 자식을 지워버렸을 때 DB에 delete 쿼리를 전달해주는 기능*이다.

말 그대로 부모에서 지워버림으로써 연관관계가 끊겨 *고아*가 되었기 때문에 DB에도 삭제 쿼리를 전달하는 방식이다.

 

적용하는 방법은 매핑 속성에 `orphanRemovel = true`속성을 주면 된다.

@Entity
public class Team {

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

    private String name;

    @OneToMany(mappedBy = "team", orphanRemovel = true)
    private List<Member> members = new ArrayList<>();
}

 

실제 동작을 확인해보자.

public class JpaMain {
    public static void main(String[] args) {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("hello");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        try {
            Parent parent = new Parent();
            Child child1 = new Child();
            Child child2 = new Child();

            parent.addChild(child1);
            parent.addChild(child2);

            em.persist(parent);

            Parent findP = em.find(Parent.class, parent.getId());
            List<Child> children = findP.getChildren();
            children.remove(0); // 자식 삭제
            tx.commit();
        } catch (Exception e) {
            tx.rollback();
        } finally {
            em.close();
        }
        emf.close();
    }
}

부모 객체에서 자식 List<>를 가져온 뒤 자식을 삭제했다. 실행하면 자동으로 delete 쿼리가 전송된다.

삭제 쿼리

 

부모에서 자식을 삭제하는 상황을 살펴보고 있다. `반대는 안될까?`

자식에서 부모를 삭제한다고 부모 테이블에 삭제 쿼리가 전송된다면 큰일난다. 부모는 여러 명의 자식을 갖기 때문에 의도치 않게 모든 연관관계가 끊어질 수 있기 때문이다.

 

따라서 고아 객체를 삭제할 수 있는 속성 `orphanRemovel`은 다대일 관계에서 `@OneToMany`에만 설정이 가능하다.

(일대일 관계에서는 `@OneToOne`에도 설정 가능)

 

고아 객체 속성을 사용할 때도 영속성 전이와 마찬가지로 *특정 부모 엔티티가 자식 엔티티를 개인 소유할 때 사용해야한다.*

 


3. 생명주기 관점으로 다시보기

영속성 전이와 고아 객체 모두 무언가 자동으로 저장해주고 삭제해주는 기능이다.

그렇다면 두 옵션을 같이 사용한다는 것은 어떤 의미가 있을까?

@Entity
public class Team {

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

    private String name;

    // 두 옵션 모두 적용
    @OneToMany(mappedBy = "team", cascade = CascadeType.ALL, orphanRemovel = true)
    private List<Member> members = new ArrayList<>();
}

 

  • 두 옵션을 모두 사용하면 부모 엔티티를 통해서 자식 엔티티의 생명주기를 관리할 수 있다.
    • 자식을 직접 `em.persist()`하거나 `em.remove()`할 필요가 없다.
    • 영속성 전이를 통해 영속시키거나 지울 때, 자식이 통째로 insert 되거나 delete 된다.
    • `orphanRemovel =  true`속성을 통해 부모 엔티티에서 특정 자식을 지우면 자동으로 DB에 delete 쿼리가 전송된다.
  • 도메인 주도 설계(DDD)의 Aggregate Root 개념을 구현할 때 유용하다.
    • 지금의 경우 Parent가 Aggregate Root, 따라서 Child의 Repository를 만들 필요가 없음

* DDD, Aggregate Root의 개념은 따로 공부하도록 하자.

 

 

출처 : 인프런, 김영한의 자바 ORM 표준 JPA 프로그래밍 - 기본편