JPA

영속성 전이(CASCADE)에 관해

쿠카이든 2022. 10. 6. 21:10
728x90
영속성 전이 : CASCADE
  • 특정 엔티티를 영속 상태로 만들때 연관된 엔티티도 함께 영속 상태로 만들고 싶을때 사용합니다.
  • eg. 부모 엔티티 저장시 자식 엔티티도 저장하게끔.
언제 쓰느냐?
  • 하나의 부모가 자식들을 관리할 때 ( 단일 엔티티에 종속적인 경우)
  • 라이프사이클이 같을때, 단일 소유자
쓰면 안되는 케이스?
  • 자식의 연관 관계가 2개 이상일때
  • 예를 들어 다음과 같은 경우에는 쓰면 안된다.

  • 그림처럼 C 라는 엔티티가 A, B 에 의해서 관리된다고 하자.
  • 이런 경우에서는 영속성 전이를 사용해서는 안된다.

참고

CASCADE 의 옵션은 ALL, PERSIST, REMOVE, MERGE, REFRESH, DETACH 가 있지만

주로 사용되는건 ALL, PERSIST, REMOVE 정도이고 실무에서 사용되는 건 ALL, PERSIST

각 옵션이 의미하는 것은 다음과 같다.

  • ALL : 모두 적용
  • PERSIST : 영속
  • REMOVE : 삭제
  • MERGE : 병합
  • REFRESH : REFRESH
  • DETACH : DETACH
PERSIST

영속한다. 쉽게 이야기하면 부모 엔티티를 저장할 때 연관된 자식 엔티티도 같이 저장합니다.

코드 예시를 하나 들어보죠.

@Entity
@Getter
@Setter
public class Parent {
    @Id @GeneratedValue
    private Long id;

    private String name;

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

    public void addChild(Child child) {
        this.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;
}
  • 위와 같이 parent, child 가 있고 parent 가 child 를 관리할 수 있도록 cascade 옵션을 PERSIST 로 줬습니다.
  • 이런 경우에 parent 엔티티가 저장될때 parent entity 에 childList 에 뭔가 새로운 값이 추가되면 이것도 같이 추가되겠죠.
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();

tx.commit();
  • 원래 우리가 엔티티를 저장할 때 엔티티마다 persist 를 해줘야 합니다.
  • 그럼 사실 코드는 아래처럼 되었어야 겠죠?
em.persist(child1);
em.persist(child2);
em.persist(parent);
  • 사실 cascade 옵션이 없었다면 em.persist() 가 3개의 엔티티를 저장하기 위해서 3번 호출이 되어야 했습니다.
  • 하지만 여기서 한번만 호출하면 충분하죠.
  • 또한 위와 같은 코드를 실행하고 로그를 확인하면 3번의 insert 쿼리가 실행됩니다.
  • 그럼 이 옵션에서 parent 삭제하면 어떻게 될까? 자식이 삭제될까?
  • 느낌으로는 자식도 삭제되야 될 것 같습니다.
em.flush();
em.clear();

Parent findParent = em.find(Parent.class, parent.getId());
em.remove(findParent);
  • 영속성 컨텍스트를 초기화하고 parent 를 가져와 삭제해보면 그럼 자식을 삭제하는 삭제 쿼리가 나갈까요?
  • 아닙니다. 심지어 에러가 발생한다. child가 parent 를 참조하고 있기 때문이죠.
  • 이를 삭제하려면 어떻게 해야될까?
REMOVE
  • 부모 엔티티를 삭제할때 연관되어 있는 자식 엔티티도 같이 삭제하는 옵션입니다.
  • Parent 클래스의 @OneToMany 의 cascade 값을 REMOVE 로 변경하고 실행하면 delete query 가 세 개가 나가 자식들도 삭제됩니다.
  • 이 뿐만 아니라 이 상태의 전이가 하위로 계속해서 발생합니다.
ALL

REMOVE + PERSIST

정리해보자!

CASCADE 는 부모의 영속성 상태를 자식에게 전달할 때 사용한다.

부모를 저장할 때, 추가된 자식을 저장하거나 부모를 삭제할 때 연관된 자식을 삭제하는 것이다.

따라서 확실하게 자식을 관리하는 엔티티가 부모 하나일때만 사용해야 하며, 라이프 사이클이 같을 때 써야한다는 것이다.

만약에 자식 엔티티를 관리하는 또 다른 엔티티가 있는데 부모 엔티티가 삭제된다고 자식 엔티티도 삭제하면 또 다른 엔티티에서 사용하지 못하는 이슈가 발생하기 때문이다.

고아 객체
  • 고아 객체라는 말이 좀 그렇긴 한데, 쉽게 이야기해서 부모 엔티티와 연관 관계가 끊어진 자식 엔티티를 말합니다.
  • 옵셥으로 부모 엔티티와 연관 관계가 끊어진 자식 엔티티를 자동으로 삭제할 수 있습니다.

orphanRemoval = true

  • 옵션을 주게 되면 부모 엔티티에서 자식 엔티티를 제거하면 delete 쿼리가 나갑니다. CascadeType.REMOVE 처럼 동작하죠.
  • 주의해야 할 점은 반드시 참조하는 곳이 하나 ( 특정 엔티티가 개인 소유할때)일 때 사용해야 합니다.
  • 그럼 여기서 CascadeType.REMOVE 랑 orphanRemoval = true 의 차이점이 뭔가에 대해서 다시 짚고 갑시다.
  • CascadeType.REMOVE 는 부모 엔티티를 삭제할 때 연관된 자식 엔티티를 삭제한다.
  • orphanRemoval = true 는 연결이 끊어진 자식 엔티티를 삭제한다.
  • 비슷하지만 다르기 때문에 명심해야 합니다.
@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST, orphanRemoval = true)
  • 옵션을 위처럼 변경하고 다시 해보자.
  • CascadeType.REMOVE 와 동일하게 동작한다.
  • 부모 엔티티를 삭제했다는 의미는 즉, 부모 엔티티와 연관을 맺고 있던 자식들이 고아가 됐다는 의미이다.
  • 그래서 이때는 REMOVE 와 동일하게 동작하는 것이다.
  • 그럼 아래처럼 해보자.
Parent findParent = em.find(Parent.class, parent.getId());
findParent.getChildList().remove(0);
  • parent 의 자식 리스트에서 하나를 삭제했다. 이때 쿼리는 어떻게 될까?
  • findParent 와 연결을 끊어버린 0번째 자식을 고아로 만든것이고 이는 즉 0번째 자식을 삭제하는 쿼리를 발생시킨다.
cascade = CascadeType.ALL + orphanRemoval = true
  • 두 옵션을 모두 활성화하면 어떻게 될까?
    • 이러면 단순히 부모 엔티티에서 자식 엔티티의 모든 생명 주기를 관리할 수 있게 된다.
    • CascadeType.PERSIST + orphanRemoval = true 의 조합과 위의 조합이 뭐가 다를까 의문이 들었다.
  • 사실 여기서 영속성 전이라는 개념에 대해서 잘 이해해야 한다.
    • 상태를 전이한다는 것은 트리처럼 연관 관계에 속한 모든 것에 계속해서 상태를 전이한다는 것을 말한다.
  • CascadeType.REMOVE 라고 해보자.
    • CascadeType.REMOVE 는 명시적인 remove 메소드 호출이 일어나지 않으면 삭제가 일어나지 않는다.
  • 예를 들어보자.
    • parent 엔티티가 @OneToMany 로 child 엔티티와 연관 관계를 맺고 있다고 한다.
    • 그럼 이 때 parent 엔티티를 가져와 child 엔티티를 명시적으로 삭제한다고 해보자. 이 때는 명시적인 remove 메소드를 호출했다.
    • 이 때는 데이터베이스에서 child 엔티티에 대한 삭제가 발생한다.
  • 하지만 단순히 parent.getchildList().clear() 로 연결을 끊어버린다고 해보자. 이 때는 데이터베이스에서는 직접 삭제가 일어나지 않는다.
    • 고아 객체를 만들어버리겠다는 의지를 보이는 코드임에도 불구함에도 말이다.
  • 하지만 orphanRemoval = true 의 경우 객체의 레퍼런스를 확인하기 때문에 REMOVE 와는 다르게 데이터베이스 삭제가 일어난다.

정리

이 부분에서 가장 헤깔릴 수 있는 부분이 CascadeType.REMOVE 와 orphanRemoval = true 가 아닐까 싶다.

부모 엔티티가 삭제되면 자식 엔티티도 삭제된다 => REMOVE

부모 엔티티에서 자식만 삭제하고 싶다 => orphanRemoval = true

정도로 이해하고 넘어가자.

연관 관계 관리
  • 모든 연관 관계는 지연 로딩으로 사용한다. OneToOne ManyToOne 은 기본 전략이 EAGER 임으로 꼭 LAZY 로 설정해줘야 한다.
  • 라이프사이클을 같이 관리하고 싶다면 영속성 전이 옵션을 CascadeType.ALL 로 지정한다(부모 엔티티와 자식 엔티티간 1:1 관계일 경우)

 

참고 : https://velog.io/@devsh/JPA-CASCADE-%EC%98%81%EC%86%8D%EC%84%B1-%EC%A0%84%EC%9D%B4

 

JPA - CASCADE , 영속성 전이

특정 엔티티를 영속 상태로 만들때 연관된 엔티티도 함께 영속 상태로 만들고 싶을때 사용합니다.eg. 부모 엔티티 저장시 자식 엔티티도 저장하게끔.하나의 부모가 자식들을 관리할 때 ( 단일 엔

velog.io

 

728x90