특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶으면 영속성 전이 기능을 사용하면 된다. 예를 들어 엔티티를 저장하거나 삭제할 때 연관된 엔티티도 힘께 저장되거나 삭제되는 경우이다.
JPA는 cascade 옵션으로 영속성 전이를 제공한다.
public enum CascadeType {
ALL, // 모두 적용
PERSIST, // 양속
MERGE, // 병힙
REMOVE, // 삭제
REFRESH, // REFRESF
DETACH // DETACH
}
예를 들어 다음과 같이 부모와 자식이 일대다 관계로 정의되어 있다고 하자.
@Entity
public class Parent {
@Id @GeneratedValue
private Long id;
@OneToMany(mappedBy = "parent")
private List<Child> children = new ArrayList<Child>();
}
@Entity
public class Child {
@Id @GeneratedValue
private Long id;
@ManyToOne
private Parent parent;
}
cascade 옵션 없이 부모 1명에 자식 2명을 저장한다면 다음과 같은 코드를 작성한다.
Parent parent = new Parent();
em.persist(parent); //부모 저장
Child child1 = new Child();
child1.setParent(parent);
parent.getChildren().add(child1);
em.persist(child1); //자식1 저장
Child child2 = new Child();
child2.setParent(parent);
parent.getChildren().add(child2);
em.persist(child2); //자식2 저장
JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태이어야 한다. 따라서 위 코드를 보면 부모 엔티티를 영속 상태로 만들고 자식 엔티티도 각각 영속 상태로 만든다. 이럴 때 영속성 전이를 사용하면 부모 엔티티만 영속 상태로 만들면 연관된 자식 엔티티까지 한 번에 영속 상태로 만들 수 있다.
@Entity
public class Parent {
@Id @GeneratedValue
private Long id;
@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
private List<Child> children = new ArrayList<Child>();
}
Parent parent = new Parent();
em.persist(parent); //부모 저장
Child child1 = new Child();
child1.setParent(parent); //연관관계 추가
parent.getChildren().add(child1);
Child child2 = new Child();
child2.setParent(parent); //연관관계 추가
parent.getChildren().add(child2);
em.persist(parent); //부모 저장, 연관된 자식들 저장
cascade를 적용하면, em.persist(parent)
를 통해 부모만 영속화하면 cascade = CascadeType.PERSIST
로 설정한 자식 엔티티까지 영속화해서 저장한다.
이때 주의할 점은 영속성 전이는 연관관계를 매핑하는 것과는 관련이 없다는 점이다. 따라서 영속성 전이를 사용하더라도 항상 양방향 연관관계를 추가해야 한다. 위 예제 코드를 보면 양방향 연관관계를 추가한 다음에 영속 상태로 만드는 것을 확인할 수 있다.
@Entity
public class Parent {
@Id @GeneratedValue
private Long id;
@OneToMany(mappedBy = "parent", cascade = CascadeType.REMOVE)
private List<Child> children = new ArrayList<Child>();
}
Parent findParent = em.find(Parent.class, 1L);
em.remove(findParent);
cascade = CascadeType.REMOVE
로 설정하고 em.remove(findParent)
로 부모 엔티티만 삭제하면 연관된 자식 엔티티까지 함께 삭제된다. 이때 delete sql이 3번 실행되어 부모 엔티티, 연관된 두개의 엔티티가 데이터베이스에서 삭제된다.
만약 cascade = CascadeType.REMOVE
를 설정하지 않고 em.remove(findParent)
를 실행하면 어떻게 될까? 그러면 부모 엔티티만 삭제된다. 그런데 데이터베이스의 부모 로우를 삭제하는 순간 자식 테이블에 걸려 있는 외래 키 제약조건으로 인해, 데이터베이스에서 외래키 무결성 예외가 발생한다.
JPA는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 제공하는데 이를 고아 객체(ORPHAN) 제거라 한다. 이 기능을 사용하면, 부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 DB에사 자동으로 삭제된다.
@Entity
public class Parent {
@Id @GeneratedValue
private Long id;
@OneToMany(mappedBy = "parent", orphanRemoval = true)
private List<Child> children = new ArrayList<Child>();
}
Parent findParent = em.find(Parent.class, 1L);
findParent.getChildren().remove(0); //컬렉션에서 첫 번째 자식을 제거
findParent.getChildren().remove(0)
로 첫 번째 자식을 제거하면 delete from child where id = ?
sql이 실행된다. 즉 orphanRemoval = true
옵션으로 인해 컬렉션에서 엔티티를 제거하면 데이터베이스의 데이터도 삭제된다.
고아 객체 제거 기능은 영속성 컨텍스트를 플러시할 때 적용된다. 따라서 플러시 시점에 delete sql이 실행된다.
정리하자면, 고아 객체 제거는 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능이다. 따라서 이 기능은 참조하는 곳이 하나일 때만 사용해야 한다. 즉 특정 엔티티가 혼자서만 소유하는 엔티티에만 이 기능을 적용해야 한다. 만약 삭제한 엔티티를 다른 곳에서도 참조한다면 문제가 발생할 수 있다. 이런 이유로 orphanRemoval은 @OneToOne, @OneToMany에서만 사용할 수 있다.
CascadeType.ALL
+ orphanRemoval = true
를 동시에 사용하면 어떻게 될까? 두 옵션을 모두 활성화하면 부모 엔티티를 통해서 자식 엔티티의 생명주기를 관리할 수 있다.
예를 들어 자식을 저장하려면 부모에만 등록하면 된다. (CASCADE)
Parent parent = em.find(Parent.class, parentId);
parent.addChild(child1);
그리고 자식을 삭제하려면 부모에서 제거하면 된다. (orphanRemoval)
Parent parent = em.find(Parent.class, parentId);
parent.getChildren().remove(removeObject);
앞서 CascadeType.ALL
+ orphanRemoval = true
를 동시에 사용하면 부모 엔티티가 자식의 생명주기를 관리한다고 했다. 그렇다면, CascadeType.ALL
만 사용했을 때와 orphanRemoval = true
까지 함께 사용했을 때의 차이는 무엇일까? 즉, CascadeType.REMOVE
와 orphanRemoval = true
의 차이는 무엇일까?
헷갈릴 수 있는 개념이지만, cascade 설정과 orphanRemoval 설정이 아예 별개라고 생각하면 덜 헷갈린다.
CascadeType.ALL
DB에서 부모 엔티티를 삭제하면, 자식 엔티티까지 DB에서 삭제된다.
parent.getChildrenList().remove(0)
의 경우, 자식 리스트의 첫번째 엔티티는 고아객체가 되더라도 DB에서 삭제되지 않으며 DB에서 외래키도 유지된다.
CascadeType.ALL
+ orphanRemoval = true
DB에서 부모 엔티티를 삭제하면, 자식 엔티티까지 DB에서 삭제된다.
parent.getChildrenList().remove(0)
의 경우, 자식 리스트의 첫번째 엔티티는 고아객체가 되고, 애플리케이션 내에서 parent의 자식 리스트에서 제거됨과 동시에 DB 내에서도 삭제된다.
cascade 옵션이 활성화되어 있으면, 영속성에 생긴 변화는 무조건 전파된다. 부모이든 자식이든, 단방향이든 양방향이든 상관없이 전파된다.
cascade 또는 orphanRemoval=true 옵션이 적용된 엔티티가 다른 엔티티를 참조하는 부모 엔티티이거나, 참조되는 자식 엔티티인 경우, cascade 또는 orphanRemoval=true 옵션에 의해 엔티티가 삭제될 때 문제가 발생할 수 있다.
따라서 여러 엔티티들과 참조 관계를 맺고 있는 엔티티에는 CASCADE 옵션과 orphanRemoval=true를 적용하지 말자.
그렇다면 어디 범위까지 cascade와 orphanRemoval=true 옵션을 적용하는 것이 좋을까? 통상적으로 권장되는 cascade의 범위는 완전히 개인 소유하는 엔티티인 경우이다. 예를 들어 게시판과 첨부파일이 있을 때, 첨부파일은 하나의 게시판 엔티티만 참조하므로 개인 소유이다.
Reference
https://www.inflearn.com/questions/31969/cascade-%EC%98%B5%EC%85%98-%EC%A7%88%EB%AC%B8