Cascade?

정민주·2024년 8월 14일

스프링 스터디

목록 보기
10/17

오늘은 헷갈리던 cascade 옵션, orphanRemoval 에 대해 정확하게 정의해보고자 한다.

일단 해당 기능이 왜 생겨나게 되었는지 알아보겠다.


0. JPA CASCADE 왜 필요할까?

어떤 JPA 엔티티는 다른 엔티티의 존재에 깊게 연관되어 있기도 한다. 가장 대표적인 예시로는 ‘댓글’과 ‘게시물’의 관계가 있다. ‘댓글’ 엔티티는 ‘게시물’ 엔티티가 없다면 존재 의의가 없다!

위의 상황을 비즈니스 로직을 작성한다고 가정해봤을 때 우린 몇 가지 의문이 생긴다.

  • 게시물을 삭제할 때 댓글을 삭제하는 로직을 번거롭게 항상 작성해 줘야 할까?
  • 실수로 댓글 삭제 로직을 빼먹을 수도 있지 않을까?
  • 보다 좋은 방법이 없을까?

이런 문제점들을 해결하기 위해 JPA Cascade가 등장했다!

JPA Cascade를 활용하면 ‘어떤 엔티티와 다른 엔티티가 밀접한 연관성이 있을 때’ 에 대한 관리가 매우 수월해진다.

즉, A라는 엔티티에 어떤 작업을 수행했을 때, 그 작업이 연관된 B라는 엔티티도 이루어져야 한다면 JPA Cascade를 유용하게 사용할 수 있다는 의미이다.

Cascade는 ‘폭포수가 흐르다’ 라는 사전적 의미를 갖고 있습니다.

본격적으로 해당 옵션에 대해 공부하기 전, 간단한 용어 정리부터 해보겠다.


0. 용어 정리

🤓영속 상태
엔티티가 EntityManager에 의해 관리되고 있는 상태이며, 그 상태가 데이터베이스와 동기화될 수 있음을 의미


  • 준영속 상태
    : 한때 영속 상태였지만 현재는 EntityManager에 의해 관리되지 않는 상태(필요시 재관리 가능)
  • 비영속 상태
    : 엔티티가 EntityManager에 의해 관리되지 않는 상태
  • 삭제 상태
    : 엔티티가 삭제될 예정

🤓 영속성 전이
JPA에서 한 엔티티의 상태 변화가 연관된 다른 엔티티에 자동으로 전파되도록 하는 기능

  • 부모-자식 관계에서 자주 사용
  • 부모 엔티티에 수행된 작업이 자식 엔티티에 자동으로 적용되도록 설정

1. Cascade?

: 특정 엔티티를 영속 상태 로 만들 경우, 연관된 엔티티들도 함께 영속 상태로 만드는 것을 의미한다. 즉 영속성 전이를 사용하는 것이다.

: JPA에서는 영속성 전이를 cascade 옵션을 통해 설정할 수 있다.
쉽게 말해 부모 엔티티를 다룰 경우 자식 엔티티까지 다룰 수 있다는 것이다.

JPA에서 사용할 수 있는 주요 영속성 전이 옵션은 다음과 같다!

  • All
    : 모든 영속성 전이 옵션을 포함
  • PERSIST
    : 부모 엔티티가 영속화될 때, 연관된 자식 엔티티들도 자동으로 영속화된다는 것을 의미
  • REMOVE
    : 부모 엔티티가 삭제될 때, 연관된 자식 엔티티들도 자동으로 삭제
  • REFRESH
    : 부모 엔티티가 새로고침될 때, 연관된 자식 엔티티들도 자동으로 새로고침
    -> 데이터베이스로부터 최신 상태를 불러올 때 사용
  • DETACH
    : 부모 엔티티가 준영속 상태로 전환될 때, 연관된 자식 엔티티들도 자동으로 준영속 상태로 전환
  • MERGE
    : 부모 엔티티가 병합될 때 연관된 자식 엔티티들도 자동으로 병합

오늘은 가장 많이 사용되는 PERSIST와 REMOVE에 대해 집중적으로 알아볼 예정이다.


2. CASCADE.PERSIST

해당 옵션에 대해 조금 더 추가설명을 해보자면, "저장"의 기능이라 보면 된다.

이는 부모 엔티티와 자식 엔티티가 모두 EntityManager에 의해 관리되고, 데이터베이스에 함께 저장될 수 있도록 해준다는 의미이다.

아래와 같은 부모, 자식 엔티티가 있다고 가정해보자.


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

    @OneToMany(cascade = CascadeType.PERSIST)
    private List<Child> children = new ArrayList<>();
}

@Entity
public class Child {
    @Id @GeneratedValue
    private Long id;
    
    private String name;
}

위의 코드에서 Parent 엔티티는 Child 엔티티와 OneToMany 관계를 가지고 있으며, 이 관계에 CascadeType.PERSIST가 설정되어 있다.

이제 다음과 같이 Parent 엔티티를 생성하고 자식 엔티티들을 추가한 후 persist()를 수행시키면,


// 부모 엔티티 생성
Parent parent = new Parent();
Child child1 = new Child();
Child child2 = new Child();

// 자식 엔티티 생성 및 부모에 추가
parent.getChildren().add(child1);
parent.getChildren().add(child2);

// 부모 엔티티를 영속화 - 이 시점에 자식 엔티티도 함께 영속화됨
entityManager.persist(parent);

위의 코드에서 parent 엔티티를 영속화(persist)할 때, 연관된 child1과 child2도 자동으로 영속화 된다. 즉, 부모 엔티티와 자식 엔티티들이 한 번에 영속화되어 데이터베이스에 저장 된다!


3. CASCADE.REMOVE

해당 옵션은 부모 엔티티가 삭제될 때, 연관된 자식 엔티티들도 자동으로 삭제된다.

마찬가지로 부모-자식 클래스가 아래와 같다고 가정하면


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

    @OneToMany(cascade = CascadeType.ALL)
    private List<Child> children = new ArrayList<>();
}
@Entity
public class Child {
    @Id @GeneratedValue
    private Long id;
    
    private String name;
}

삭제 기능을 위해선 우선적으로 저장이 되어야 하므로, 편의상 cascade.ALL로 설정하였다.

cascade를 사용하지 않는 삭제 방법은 아래와 같다.

// 부모 엔티티와 자식 엔티티 조회
Parent parent = entityManager.find(Parent.class, 1L);
Child child1 = entityManager.find(Child.class, 1L);
Child child2 = entityManager.find(Child.class, 2L);

// 자식 엔티티 삭제
entityManager.remove(child1);
entityManager.remove(child2);

// 부모 엔티티 삭제
entityManager.remove(parent);

해당 코드를 CascadeType.REMOVE를 사용하여 단순화하면?

// 부모 엔티티 조회 (자식 엔티티들은 CascadeType.REMOVE에 의해 자동으로 삭제됨)
Parent parent = entityManager.find(Parent.class, 1L);

// 부모 엔티티 삭제 - 자식 엔티티들도 함께 삭제됨
entityManager.remove(parent);

CascadeType.REMOVE를 사용하여 Parent 엔티티를 삭제하면, 연관된 Child 엔티티들도 자동으로 삭제되는 걸 볼 수 있다!


4. orphanRemoval = true

해당 속성은 Cascade.REMOVAL과 굉장히 유사한 듯 다르다.

cascade.REMOVAL은 부모 객체가 "삭제" 상태일 때, 자식 객체도 모두 삭제시켜주는 기능이었다.

그러나 해당 옵션은

부모 엔티티에서 자식 엔티티와의 "연관관계" 삭제 시,
자신을 참조해주는 부모 객체가 모두 사라진 자식 객체는 "고아 객체" 가 되게 된다.

이런 고아 객체들을 자동삭제 해주는 기능이 바로 해당 기능이다!

4-1. orphanRemoval의 등장배경?

왜 등장했을 까?

부모 엔티티가 삭제되지 않고, 단순히 자식 엔티티와의 관계만 끊는 경우에 casacde.REMOVAL 옵션은, 자식 엔티티가 데이터베이스에 남아 있게 된다.

이 경우, 데이터베이스에 존재하는 데이터가 실제 비즈니스 로직과 일치하지 않게 되어 무결성 문제가 발생할 수 있다.

또한 해당 자식 엔티티는 불필요한 데이터기에 해당 옵션을 사용하면 깔끔한 관리가 되게 된다.


5. 어떤 기준을 통해 CASCADE 옵션을 적용시키나?

이렇게 편한 CASCADE! 당연히 남발해선 안된다.

통상적으로 권장하는 cascade 범위는, 완전히 개인 소유하는 엔티티일 때 이다.


예를 들어서 게시판과 첨부파일이 있을 때 첨부파일은 게시판 엔티티만 참조하므로, 개인 소유 이다.

"개인 소유" 라는 것에 대해 더 자세히 적어보자면 부모-자식 구조가 명확한 경우이다.

그런데 만약 하나의 자식에 여러 부모가 대응되는 경우엔 사용하지 않는 게 좋다.
-> 여러 부모가 대응 되는 순간 각 부모의 CASCADE 옵션들이 충돌하게 될 확률이 높아지고. 원하는 대로 로직이 흘러가지 않을 가능성이 높다.
(필자도 아무것도 모르고 N:M 관계에서 해당 옵션을 적용하려다가 피를 보고 이 게시글을 적게 되었다..)


해당 CASCADE 옵션을 설정할 때는 2개의 엔티티 중 어느 곳에 위치시킬지 정하는 것이 중요하다.
어떤 사람은 1:N의 경우에서, 1에 해당하는 엔티티에 해당 속성을 위치시키라고 말한다.

유용하지만 위험한 JPA CASCADE! 잘 알아보고 써야겠다.


참고

0개의 댓글