영속성 전이는 특정 엔티티를 영속화(persist)할 때, 연관된 다른 엔티티도 함께 영속화되도록 도와주는 기능이다.
예를 들어Team과Member가 1:N 관계일 때, 팀을 저장할 때 연관된 멤버도 함께 저장하고 싶다면 어떻게 해야 할까?
아래는 Parent 와 Child 가 1:N 관계로 매핑된 엔티티 예시이다.
부모 엔티티(Parent)가 자식 엔티티(Child)를 리스트로 관리하고 있다.
@Entity
@Getter
@Setter
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);
}
}
@Entity
@Getter
@Setter
public class Child {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
}
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();
위 코드에서는 parent, child1, child2 를 각각 영속성 컨텍스트에 등록해야 한다.
하지만 이 관계에 cascade = CascadeType.ALL 을 설정하면, 부모 객체만 영속화 해도 자식 객체가 함께 저장된다.
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List<Child> childList = new ArrayList<>();
이제 em.persist(parent) 만 호출해도 child1, child2 가 함께 저장된다.
이처럼 부모 엔티티의 상태 변경이 자식 엔티티에게 전이되는 것을
"영속성 전이(Cascade)" 라고 하며,
영속성 전이는 "연관관계 매핑" 과는 별개의 기능이며, 엔티티의 생명주기를 함께 관리하고자 할 때 주로 사용된다.
연관관계를 "어떻게 매핑하느냐" 와는 무관하며,
저장·삭제 시 편의성을 높이기 위한 별도의 기능이다.
따라서 연관관계 매핑만으로는 전이가 발생하지 않으며,
Cascade 옵션을 명시적으로 설정해야 한다.
부모 엔티티와의 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능이다.
즉, 부모 엔티티에서 자식을 컬렉션에서 제거하거나, 자식 엔티티의 부모 필드를 null로 만들면,
해당 자식은 "고아 객체" 가 되고, DB에서도 삭제된다.
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
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());
findParent.getChildList().remove(0);
tx.commit();
위와 같이 orphanRemoval = true 를 설정해주고 Parent 에서ChildList 의 첫번째 컬렉션을 remove 해주면 delete 쿼리가 발생한다.

실제 DB에서도 삭제된걸 확인할 수 있다.

1. 참조하는 곳이 하나일 때만 사용해야한다
연관관계가 끊기면 삭제되기 떄문인데, 고아 객체 제거는 부모 엔티티와 자식 엔티티의 연관관계가 끊기는 순간 DELETE 쿼리가 발생한다.
parent1.getChildren().remove(child);
→ child와의 연관관계 끊김 → child는 orphan → 삭제
만약 여러 객체가 Child를 참조하고 있다면?
Parent1 → Child
Parent2 → Child
Parent1 이 removeChild(child) 하면 child 가 삭제된다.
하지만 Parent2 입장에선 원하지 않아도 참조하던 엔티티가 삭제되버린다.
그래서 고아객체 제거는
"자식 엔티티가 오직 하나의 부모에게만 소속되어 있는 경우" 만 안전하게 사용 가능하다.
2. 왜 @OneToOne, @OneToMany만 지원할까?
위에서 설명한 내용과 이어지는 내용인데,
@ManyToOne, @ManyToMany 에서는 다대다/다대일 관계이기 때문에 고아 객체를 누군가 다른 곳에서도 참조 중일 수 있다.
예를 들어서
@ManyToMany
List<Student> students;
학생 한명이 여러 수업에 소속되어 있을 수 있다. 그런데 한 수업에서만 빠졌다고 해서 해당 학생을 DB에서 삭제 해버리면 문제가 발생한다.
참고: 개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고
아 객체 제거 기능을 활성화 하면, 부모를 제거할 때 자식도 함께
제거된다. 이것은 CascadeType.REMOVE 처럼 동작한다.
orphanRemoval = true 는 부모엔티티가 자식엔티티를 완전히 소유할때만 사용해야한다. 자식엔티티가 여러 곳에서 참조될 수 있는 구조에서는 연관관계가 끊긴다고 해서 바로 삭제하면 다른 참조 관계에도 영향을 주는 심각한 문제가 발생할 수 있다.
그래서 JPA는 이 기능을 @OneToOne, @OneToMany 에서만 지원하며,
자식의 생명주기를 부모가 전적으로 관리하는 경우에만 사용하는 것이 안전하다.
CascadeType.ALL + orphanRemoval=true
엔티티의 관계에서 cascade = CascadeType.ALL 과 orphanRemoval = true 를 함께 사용하면
부모 엔티티 하나를 통해 자식 엔티티의 생성부터 삭제까지 생명주기를 일관성 있게 관리할 수 있다.
CascadeType.ALL
부모를 persist() 하면 자식도 함께 저장되고
부모를 remove() 하면 자식도 함께 삭제됨
→ 자식의 영속 상태 전이
orphanRemoval = true
부모와의 연관관계가 끊기면 자식은 자동으로 삭제됨
→ 자식의 자동 제거
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> children = new ArrayList<>();
이렇게 설정해두면 다음과 같은 일들이 가능하다:
즉, 자식은 부모의 생명주기에 완전히 의존하게 된다.
CascadeType.ALL 과 orphanRemoval = true 를 함께 설정하면
부모 엔티티를 통해 자식 엔티티의 생명주기를 일관성 있게 관리할 수 있다.
이는 도메인 주도 설계(DDD) 에서 Aggregate Root 가 내부 구성요소의 생명주기를 책임지는 구조와 잘 맞아떨어지며,
실무에서도 자식이 부모에게 전적으로 종속되는 경우에 매우 유용하게 사용될 수 있을것 같다.