영속성 전이는 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용하는 기능이다. 즉시 로딩, 지연 로딩과는 전혀 관련이 없다. 말 그대로 부모를 저장할 대 관련된 자식을 다 같이 저장하고 싶을 때 사용한다.
예를 들어, 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장하고 싶은 경우에 사용한다.
다음과 같이 Parent와 Child 엔티티가 설계되어 있다고 가정하자.
@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);
}
}
@Entity
public class Child {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
}
이 상태에서 Parent와 Child를 저장하려면 다음과 같이 세 번 persist()를 호출해야 한다.
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); // 번거로움
이렇게 하면 좀 귀찮아진다. 이때 사용하는 게 CASCADE이다.
@Entity
public class Parent {
@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
private List<Child> childList = new ArrayList<>();
}
이렇게 설정하고 실행하면 em.persist(parent)만 해도 child 둘 다 자동으로 persist된다.
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent); // child1, child2도 자동으로 persist됨
영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없다.
단지 엔티티를 영속화 할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐이다.
보통 ALL이나 PERSIST를 많이 사용한다.
그렇다고 모든 일대다 관계에 CASCADE를 거는건 아니다. 하나의 부모가 자식들을 관리할 때만 사용해야 한다.
사용해도 되는 경우:
사용하면 안 되는 경우:
댓글 예시
회원이 게시글에 댓글을 남긴다고 가정할 때, 댓글은 게시글과도 연관되어 있고 회원과도 연관되어 있다. 이런 경우 댓글의 생명주기는 게시글에만 의존하는가? 아니면 회원에도 의존하는가?
일반적으로 댓글은 게시글에 종속되므로 게시글에서만 CASCADE를 사용할 수 있다. 하지만 만약 회원이 탈퇴해도 댓글이 남아있어야 한다면, 댓글은 독립적인 생명주기를 가져야 하므로 CASCADE를 사용하지 않는 것이 좋다.
고아 객체는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 말한다.
JPA는 고아 객체를 자동으로 삭제하는 기능을 제공한다.
orphanRemoval = true 옵션을 사용하면 된다.
@Entity
public class Parent {
@OneToMany(mappedBy = "parent", orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
}
Parent parent1 = em.find(Parent.class, id);
parent1.getChildren().remove(0); // 컬렉션에서 첫 번째 자식 제거
컬렉션에서 제거한다는 의미
Parent의 childList에서 0번 인덱스의 Child를 제거하면, 해당 Child는 더 이상 Parent와 연관관계가 없어진다.
이때orphanRemoval = true옵션이 설정되어 있으면, JPA는 이 child를 고아 객체로 판단하고 자동으로 DELETE 쿼리를 실행한다.
실행되는 쿼리:
DELETE FROM CHILD WHERE ID = ?
@OneToOne, @OneToMany만 가능하다.참고
개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고아 객체 제거 기능을 활성화하면, 부모를 제거할 때 자식도 함께 제거된다. 이것은CascadeType.REMOVE처럼 동작한다.
CascadeType.ALL + orphanRemoval=true를 함께 사용하면 부모 엔티티를 통해서 자식의 생명주기를 완전히 관리할 수 있다.
@Entity
public class Parent {
@OneToMany(mappedBy = "parent",
cascade = CascadeType.ALL,
orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
}
일반적으로 엔티티는 스스로 생명주기를 관리한다.
em.persist()로 영속화em.remove()로 제거하지만 두 옵션을 모두 활성화 한다면 다음과 같이 부모를 통해서 자식의 생명주기를 관리할 수 있다.
// 자식 저장: parent에 추가만 하면 자동 저장 (CASCADE)
parent.addChild(child);
em.persist(parent); // child도 자동으로 persist
// 자식 삭제: parent에서 제거만 하면 자동 삭제 (orphanRemoval)
parent.getChildren().remove(0); // child 자동으로 delete
// 부모 삭제: 자식도 함께 삭제 (CASCADE + orphanRemoval)
em.remove(parent); // 모든 child도 자동으로 delete
CASCADE.ALL과orphanRemoval=true의 차이
CASCADE.ALL은 부모의 영속성 상태 변화를 자식에게 전파하는 것이다(저장, 삭제 등).orphanRemoval=true는 부모와의 관계가 끊어진 자식을 자동으로 삭제하는 것이다.CASCADE.ALL만 있으면 컬렉션에서 제거해도 DB에서는 삭제되지 않지만,orphanRemoval=true가 있으면 컬렉션에서 제거하는 순간 DB에서도 삭제된다.
이 패턴은 도메인 주도 설계(DDD)의 Aggregate Root 개념을 구현할 때 유용하다.
Aggregta Root
DDD에서 여러 엔티티를 하나의 집합으로 묶어서 관리할 때, 그 집합을 대표하는 루트 엔티티를 말한다.
예를 들어, 주문(Order)과 주문상품(OrderItem)이 있을 때, Order가 Aggregate Root가 되고, OrderItem의 생명주기는 Order를 통해서만 관리하는 방식이다. 이렇게 하면 리포지토리는 Order만 있으면 되고, OrderItem은 Order를 통해서만 접근하게 된다.