특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶으면 영속성 전이(transitive persistence)를 사용하면 된다.
JPA는 CASCADE 옵션으로 영속성 전이를 제공한다. 쉽게 말해, 영속성 전이를 사용하면, 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장할 수도 있고, 부모 엔티티를 삭제할 때 자식 엔티티도 함께 삭제할 수 있다.
참고로, PERSIST, REMOVE는 em.persist()
나 em.remove()
를 실행할 때, 바로 전이가 발생하지 않고 플러시를 호출할 때 전이가 발생한다.
@Entity
public class Parent {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "parent")
private List<Child> childList = new ArrayList<>();
}
@Entity
public class Child {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "parent_id")
private Parent parent;
}
Parent parent = new Parent();
em.persist(parent);
Child child1 = new Child();
child1.setParent(parent);
parent.getChildList().add(child1);
em.persist(child1); // 자식1 저장
Child child2 = new Child();
child2.setParent(parent);
parent.getChildList().add(child1);
em.persist(child2); // 자식2 저장
위의 예제에서는 자식 엔티티도 직접 영속화해줘야 했다. 하지만, 영속성 전이 기능을 활용하면 그렇게 하지 않아도 된다.
@Entity
public class Parent {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
private List<Child> childList = new ArrayList<>();
}
Parent 클래스에서 이렇게 CascadeType.PERSIST 로 설정하게 되면, 해당 부모 엔티티와 연관된 자식 엔티티들도 같이 영속성 컨텍스트에 저장된다. 즉, 아래처럼 작성해도 자식 엔티티는 저장된다.
Parent parent = new Parent();
Child child1 = new Child();
child1.setParent(parent);
parent.getChildList().add(child1);
Child child2 = new Child();
child2.setParent(parent);
parent.getChildList().add(child2);
em.persist(parent);
영속성 전이 기능 없이 부모 엔티티와 자식 엔티티를 모두 제거하려면 다음 코드처럼 각각의 엔티티를 하나씩 제거해야 한다.
Parent findParent = em.find(Parent.class, 1L);
Child findChild1 = em.find(Child.class, 1L);
Child findChild2 = em.find(Child.class, 2L);
em.remove(findParent);
em.remove(findChild1);
em.remove(findChild2);
하지만, CascadeType.REMOVE
를 활용한 영속성 전이를 활용한다면, 부모 엔티티만 삭제함으로써 연관된 자식 엔티티도 함께 삭제할 수 있다.
Parent findParent = em.find(Parent.class, 1L);
em.remove(findParent);
코드를 실행하면, DELETE SQL
을 3번 실행하고 부모는 물론, 연관된 자식도 모두 삭제된다. 삭제 순서는 외래키 제약조건을 고려해서 자식을 먼저 삭제하고, 부모를 삭제한다.
만약, CascadeType.REMOVE
를 설정하지 않고 이 코드를 실행하게 되면 어떻게 될까? 그러면 부모 엔티티만 삭제된다. 하지만, 데이터베이스의 부모 로우만 삭제하는 순간, 자식 테이블에 걸려 있는 외래 키 제약조건으로 인해, 데이터베이스의 외래 키 무결성 예외가 발생한다.
// 데이터베이스의 외래 키 무결성 예외
org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException: Referential integrity constraint violation: "FKLH67J1N7X7GT59U0PBKWQH6O6: PUBLIC.CHILD FOREIGN KEY(PARENT_ID) REFERENCES PUBLIC.PARENT(ID) (CAST(1 AS BIGINT))"; SQL statement:
/* delete org.example.domain. [23503-214]
JPA는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 제공하는데, 이를 고아 객체(ORPHAN) 제거 라고 한다.
이 기능을 사용하면, 부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제되도록 할 수 있다.
@Entity
public class Parent {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "parent", orphanRemoval = true)
private List<Child> childList = new ArrayList<>();
}
Parent findParent = em.find(Parent.class, 1L);
findParent.getChildList().remove(0);
즉, orphanRemoval = true
옵션을 통해 컬렉션에서 엔티티를 제거하면 데이터베이스의 데이터도 삭제된다. 고아 객체 제거 기능은 영속성 컨텍스트를 플러시할 때 적용되므로 플러시 시점에 DELETE SQL이 실행된다.
고아 객체 제거 기능을 이용할 때, 모든 자식 엔티티를 제거하려면 parent.getChildList().clear();
처럼 컬렉션을 비우면 된다.
고아 객체 제거는 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고, 삭제하는 기능이다. 따라서 이 기능은 참조하는 곳이 하나일 때만 사용해야 한다!!
쉽게 이야기해서, 특정 엔티티가 개인 소유하는 엔티티에만 이 기능을 적용해야 한다. 만약 삭제한 엔티티를 다른 곳에서도 참조한다면 문제가 발생할 수 있다. 이런 이유로 orphanRemoval
은 @OneToOne
, @OneToMany
에서만 사용 가능하다.
그리고 고아 객체 제거에는 기능이 한 가지 더 있다. 개념적으로 볼 때, 부모를 제거하면 자식은 고아가 된다. 따라서 부모를 제거하면 자식도 같이 제거된다. 따라서, 고아 객체 제거 기능을 사용하면 CascadeType.REMOVE
기능도 포함되어 있다.
CascadeType.ALL
과 orphanRemoval = true
옵션을 동시에 사용하면 어떻게 될까? 일반적으로 엔티티는 EntityManager.persist()를 통해 영속화되고 EntityManager.remove()를 통해 제거된다.
그런데 두 옵션(CascadeType.ALL
과 orphanRemoval = true
)을 모두 활성화하면, 부모 엔티티를 통해서 자식의 생명주기를 관리할 수 있다. 예를 들면 다음과 같다.
// 자식을 저장하려면 부모에 등록만 하면 된다 (CASCADE.PERSIST)
Parent parent = em.find(Parent.class, 1L);
parent.addChild(child1);
// 자식을 삭제하려면, 부모에서 제거하면 된다 (orphanRemoval)
Parent parent = em.find(Parent.class, 1L);
parent.getChildList.remove(child1);
영속성 전이는 DDD의 Aggregate Root 개념을 구현할 때 사용하면 편리하다.
Aggregate 란?
DDD(Domain Driven Design)
에서Aggregate
는 도메인 모델링에서 중요한 개념 중 하나입니다.Aggregate
는 데이터와 해당 데이터를 관리하는 로직의 그룹으로 정의됩니다.Aggregate
내부의 데이터와 로직은 외부에서 접근이 가능한 단일 단위로 존재이다.Aggregate Root 란?
Aggregate root
는 Aggregate 내부에서 가장 핵심적인 객체를 의미합니다.Aggregate root
는 다른 Aggregate 객체들과 관계를 맺는 진입점 역할을 합니다.
즉,Aggregate root
는 Aggregate 내부에서 다른 객체들과의 관계를 정의하고, 해당 Aggregate를 관리하는 데 필요한 비즈니스 규칙과 로직을 구현합니다.예시
주문 시스템에서
주문(Order)
과주문 항목(OrderItem)
을Aggregate
로 정의할 수 있습니다. 이때,주문(Order)
은Aggregate root
가 되며,주문 항목(OrderItem)
은 주문(Order)에 속해있는 객체로 볼 수 있습니다.
Aggregate root
인 주문(Order)은주문 항목(OrderItem)
을 관리하며, 주문(Order)의 식별자(OrderId)를 통해 다른 객체들과 관계를 맺습니다.
자바 ORM 표준 JPA 프로그래밍
https://eocoding.tistory.com/36
https://blog.decorus.io/engineering/domain%20driven%20design/2022/05/06/design-and-management-of-aggregate-root-ddd.html