Cascade와 OrphanRemoval 알아보기

Glen·2023년 6월 24일
0

배운것

목록 보기
18/37

서론

부모 엔티티를 저장할 때 연관된 자식 엔티티도 함께 저장하고 싶다면 다음과 같은 과정을 거쳐야 한다.

Parent parent = new Parent();  
Child child = new Child(parent);  
  
parent.addChild(child);

parentRepository.save(parent); 

ParentList<Child> 필드를 가지고 있다.

연관관계 매핑은 Child를 연관관계의 주인으로 설정하여 다대일 양방향 연관관계를 매핑했다.

위와 같이 Parent를 영속한다고, Parent가 소유하고 있는 Child는 영속되지 않는다.

Child 엔티티를 영속하려면 명시적으로 Child 엔티티를 영속시켜야 한다.

Parent parent = new Parent();  
Child child = new Child(parent);  
  
parent.addChild(child); // 아무런 기능도 하지 않는다...

// 주의! 여기서 순서가 바뀌면 UPDATE 쿼리가 추가로 발생한다!
parentRepository.save(parent); 
childRepository.save(child);

ParentaddChild() 메소드는 아무런 기능도 하지 않는다.

해당 메소드는 비즈니스 로직 상 어떠한 유의미한 기능을 위해 사용될 수 있겠지만, 관련된 엔티티를 같이 영속하는 기능은 제공하지 못한다.

하지만 사람이 보기엔 Parent 엔티티가 영속될 때, 소유한 Child 엔티티도 같이 영속되는 것이 자연스럽다.

어떻게 하면 Parent 엔티티를 영속할 때, 소유한 Child 엔티티도 함께 영속시킬 수 있을까?

본론

Cascade

Cascade를 사용하면 이것이 가능해진다.

다음과 같이 연관관계 매핑 어노테이션의 속성으로 cascade를 설정하면 된다.

@Entity  
public class Parent {  
    ...
    @OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)  
    private List<Child> children = new ArrayList<>();  
    ...
}

cascade 옵션은 일대다 관계뿐 아니라, 다대일 관계에서도 설정할 수 있다.

그리고 다음과 같이 Parent를 영속할 때, Child도 함께 영속할 수 있다.

Parent parent = new Parent();  
Child child = new Child(parent);  

parent.addChild(child);

parentRepository.save(parent); // Parent가 소유한 Child도 영속된다.

단순히 영속할 때 말고, 영속 상태가 변경될 때도 cascade를 사용하면 편하게 영속 상태를 관리할 수 있다.

CascadeType은 열거형으로, 다음과 같은 열거 타입이 있다.

ALL, PERSIST, MERGE, REMOVE, REFRESH, DETACH

ALL은 말 그대로 모든 상태에 대해 작동한다.

나머지는 해당 영속 상태에 작동한다.

주로 사용되는 것은 PERSISTREMOVE 2가지이다.

Parent 엔티티를 삭제할 때, 연관된 Child도 함께 삭제하려면 CascadeType.REMOVE를 추가하면 된다.

@OneToMany(mappedBy = "parent", cascade = {CascadeType.PERSIST, CascadeType.REMOVE})  
private List<Child> children = new ArrayList<>();  

그리고 다음과 같이 Parent 엔티티를 삭제하면 연관된 Child 엔티티도 삭제된다.

Parent parent = new Parent();  
Child child = new Child(parent);  
  
parent.addChild(child);  
  
parentRepository.save(parent);  
  
// flush
  
parentRepository.delete(parent);

즉, Cascade 설정으로 자식 엔티티의 생명 주기를 부모 엔티티가 관리하는 것이 가능하다.

그렇다면 여기서 ParentList<Child> 필드에서 특정 Child를 제거한다면 해당 Child 엔티티가 삭제될 것이라 기대할 수 있다.

Parent parent = new Parent();  
Child child = new Child(parent);  
  
parent.addChild(child);  
  
parentRepository.save(parent);  
childRepository.save(child);  

// flush
  
parent.removeChild(child); // parent.getChildren().remove(child);

하지만 기대와 달리, 해당 Child 엔티티는 삭제되지 않는다.

왜냐하면 Cascade 옵션은 자신의 영속 상태를 전파해 주기 때문에, 연관관계를 맺은 엔티티의 참조가 없어지더라도 영속 상태가 반영되지 않는다.

OrphanRemoval

이때 orphanRemoval 속성을 사용하면 기대하는 기능을 누릴 수 있다.

다음과 같이 연관관계 매핑 어노테이션의 속성으로 orphanRemoval를 설정하면 된다.

@OneToMany(mappedBy = "parent", cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, orphanRemoval = true)  
private List<Child> children = new ArrayList<>();

그리고 ParentList<Child> 필드에서 특정 Child를 제거하면 해당 Child 엔티티가 삭제된다.

Parent parent = new Parent();  
Child child = new Child(parent);  
  
parent.addChild(child);  
  
parentRepository.save(parent);  
childRepository.save(child);  

// flush
  
parent.removeChild(child); // DELETE 쿼리 발생

Cascade + OrphanRemoval

Cascade와 OrphanRemoval 옵션은 주로 같이 사용하는데, 이때 주의할 점이 있다.

orphanRemoval 옵션을 사용할 때, CascadeType.REMOVE를 사용하지 않아도 같은 효과가 적용된다.

@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST, orphanRemoval = true)  
private List<Child> children = new ArrayList<>();
Parent parent = new Parent();  
Child child = new Child(parent);  
  
parent.addChild(child);  
  
parentRepository.save(parent);  
  
// flush
  
parentRepository.delete(parent); // Child도 함께 DELETE 된다.

어찌 보면 당연한 결과라고 생각할 수 있지만, Parent가 삭제되더라도, Child는 삭제되지 않길 원할 때 예상하지 못한 상황이 생길 수 있으므로 주의해야 한다.

또한, CascadeType.PERSIST 속성을 사용하지 않고 orphanRemoval 속성을 사용하면 orphanRemoval 기능이 작동하지 않는다.

@OneToMany(mappedBy = "parent", orphanRemoval = true)  
private List<Child> children = new ArrayList<>();
Parent parent = new Parent();  
Child child = new Child(parent);  
  
parent.addChild(child);  
  
parentRepository.save(parent);  
childRepository.save(child);  
  
em.flush();  
  
parent.remove(child); // DELETE 쿼리가 발생하지 않는다.

해당 이슈는 JPA의 구현체인 하이버네이트의 버그이기 때문에 CascadeType.PERSIST가 적용되어야 동작한다고 한다.

profile
꾸준히 성장하고 싶은 사람

0개의 댓글