8 min read

<JPA> 알고 쓰는 Cascade(영속성 전이)

이번 포스팅에서는 쓰면서도 헷갈리던 JPA의 Cascade 옵션에 대해서 정리하겠습니다.일단 기본적으로 Cascade라는 옵션이 등장하게 된 배경부터 알아봅시다.

아래의 코드를 보시죠

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

    private String name;

    @OneToMany(mappedBy = "parent")
    private List<Child> childes = new ArrayList<>();

    public void addchild(Child child) {
        childes.add(child);
        child.setParent(this);
    }
}
@Setter
@Getter
@Entity
public class Child {
    @Id
    @GeneratedValue
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "parent_id")
    private Parent parent;

}

			Child child1 = new Child();
            Child child2 = new Child();

            Parent parent = new Parent();
            parent.addchild(child1);
            parent.addchild(child2);

            em.persist(parent);
            em.persist(child1);
            em.persist(child2);

지금 현재 Parent와 child는 양방향 연관관계로 맺어진 상황입니다.여기서 세가지 인스턴스를 모두 영속화 시키기 위해서는 em.persist()를 3번 호출해서 Parent와 Child 각각 모두 영속화 시켜주는 작업을 진행해야합니다.

여기서 다음과 같은 의문이 듭니다.Parent가 관리하는 Child 인스턴스의 경우 Parent를 em.persist()할 때 같이 진행하면 편하지 않을까?이러한 질문을 해결할 수 있는 옵션이 바로 Cascade,영속성 전이입니다.

정리해보겠습니다.

위의 예제에서 Parent와 Child는 일대다 양방향 연관관계인 상황입니다.여기서 Parent를 em.persist할때 Parent의 필드값인 childes도 마찬가지도 em.persist해주겠다는 의미입니다.그러면 아래와 같이 실행할 코드가 바뀌어야 합니다.

	@OneToMany(mappedBy = "parent",cascade = CascadeType.ALL)
    private List<Child> childes = new ArrayList<>();
			Child child1 = new Child();
            Child child2 = new Child();

            Parent parent = new Parent();
            parent.addchild(child1);
            parent.addchild(child2);

            em.persist(parent);

Cascade라는 영어단의 의미 “연쇄”를 잘 생각해보시면 쉽게 이해할 수 있을 것 입니다.Cascade의 경우 아래와 같이 6가지의 종류가 있습니다.

  • ALL
  • PERSIST
  • REMOVE
  • MERGE
  • REFERESH
  • DETACH

하지만 정작 주로 사용하게 되는 것은 ALL 또는 PESIST입니다. ALL의 경우의 위의 해당하는 모든 영속성이 전이되는 경우이고 Persist의 경우 엔티티가 저장될때만 연쇄적으로 저장되게 하는 옵션입니다.

Cascade를 어디에 써야 하나?

일대다 연관관계 기준으로 설명드리겠습니다.연관관계의 주인이 항상 多에 존재한다고 알고 계실것입니다.이의 반대쪽 엔티티에 사용하면 됩니다.즉 多가 아닌 一로 시작하는 곳이라고 생각하시면 됩니다.사실 별게 아닌게 @OneToMany가 걸려있는 엔티티이기 때문에 @OneToMany가 걸린 엔티티에 거신다고 생각하시면 됩니다.

Cascade를 언제 써야 하나?

편안한게 그냥 OneToMany에 전부 다 넣으면 되냐라고 생각하실 수도 있습니다.하지만 사실 이 질문이 가장 어렵고 가장 중요하다고 봅니다.이는 현재 제가 진행중인 프로젝트를 기준으로 설명해보겠습니다.다음 그림은 해당 프로젝트의 ERD 일부분입니다.

Post라는 엔티티를 기준으로 많은 엔티티들이 얽혀있는 것을 볼 수 있습니다.이러한 상황에서 Cascasde는 어떤 엔티티에게 달 수 있을까요?아래의 기준이 있습니다.

  1. Cascade되는 엔티티와 Cascade를 설정하는 엔티티의 라이프사이클이 동일하거나 비슷해야한다.
  2. Cascade되는 엔티티가 Cascade를 설정하는 엔티티에서만 사용되어야 한다.

위의 두 조건을 DB 설계를 안해보신분들이 보시면 상당히 어려울 수도 있습니다.위의 Post와 Image를 예로 들어보겠습니다.Image의 경우 Post게시글에 포함되는 사진을 의미합니다.즉,게시글에 첨부되는 사진들이죠.이러한 사진들의 경우 게시글이 생성될때 함께 생성되고 삭제될때 함께 삭제됩니다.더군다나 이러한 사진은 게시글에서 랜더링 되는 목적이외에 어떠한 곳에서도 사용되지 않습니다.

결론적으로 Image와 Tag가 양방향인 상황에서 Image 엔티티의 List<Image> iamge@OneToMany옵션에 Cascade.ALL을 걸어도 아무런 관계가 없다는 뜻입니다.

반대로 Comment와 Post를 Cascade를 걸 경우를 보겠습니다.댓글이 달린 게시글이 삭제될 경우,유저는 본인이 쓴 댓글을 더이상 확인할 수 없게 되는것입니다.물론 프로덕트의 설계 상 유저가 본인이 쓴 댓글이 필요가 없으면 Cascade를 걸어도 되겠지만 그렇지 않은 경우라면 걸면 안될것입니다.

Cascade는 생각보다 별 것 아니지만 그렇다고 해서 생각없이 모든 OneToMany에 걸어 버리면 운영에 많이 힘들어집니다.

orphanRemoval과 고아객체

이번에는 Cascade와 비슷한 orphanRemoval 옵션에 대해서 알아보겠습니다.이 또한 마찬가지로 OnetoMany나 OnetoOne에 사용가능합니다.부모와 연관관계가 끊어진 엔티티를 고아객체라하며 이러한 고아객체를 자동으로 삭제해주는 옵션을 활성화 시키는것이 orphanRemoval=true옵션을 @ OnetoMany에 걸어주면 되는 것입니다.해당 옵션이 걸린 상황을 코드를 통해 봅시다.

			Child child1 = new Child();
            Child child2 = new Child();

            Parent parent = new Parent();
            parent.addchild(child1);
            parent.addchild(child2);

            em.persist(parent);
            parent.getChildes().remove(child1);

            em.flush();
            em.clear();

            Parent findParent = em.find(Parent.class, parent.getId());
            Child child = em.find(Child.class, child2.getId());
            findParent.getChildes().remove(child);

            tx.commit();

em.flush()를 하기전과 후와 코드가 나뉩니다.돌려보시면 delete쿼리가 두개가 발생하는 것을 볼 수 있습니다.flush전에는 child1 엔티티가 삭제되는 것이고 후에는 child2가 삭제되는 것을 확인 할 수 있습니다.

그러면 orphanRemoval=true옵션을 언제 사용해야 할까요?이 또한 Cascade와 동일한 맥락입니다.라이프 사이클이 동일하고 해당 엔티티에서만 쓰이는 엔티티일 경우 사용해주면 됩니다.

orphanRemoval=true와 Cascade.REMOVE 차이

필자가 처음 이 개념을 접했을때는 위의 차이를 전혀 구분하지 못했습니다. Cascade.REMOVE의 경우 一에 해당하는 엔티티를 em.remove를 통해 직접 삭제할 때,그 아래에 있는 多에 해당하는 엔티티들이 삭제되는 것입니다.

orphanRemoval=true는 위의 경우는 물론,엔티티의 리스트에서 요소를 삭제하기만 해도 해당 엔티티가 delete되는 기능까지 포함하고 있다고 이해하시면 됩니다.

orphanRemoval=true와 Cascade.ALL 함께 쓰기

만약 위와 같은 조합으로 옵션을 주게 된다면 多에 해당하는 리스트 형태의 엔티티는 一에 해당하는 엔티티와 라이프 사이클을 완전히 함께할뿐더러 다른 엔티티에서는 사용되지 않는 엔티티일것입니다.

즉,부모 엔티티를 통해서 자식의 생명주기가 관리가능하다는 의미입니다.

위에서 들었던 예시로 게시글과 사진이 존재할 경우,사진에 대한 repository조차 필요가 없다는 의미입니다.왜냐하면 모든 생명주기는 게시글 repository에서 관리하고 게시글 service를 통해 생성되고 삭제될 수 있기 때문입니다.

이렇게 해서 Cascade와 orphanRemoval에 대해서 알아보았습니다.필자도 무지성으로 옵션 넣는게 좋아보여서 무작정 넣어서 프로젝트를 돌린적이 있었는데 너무나 당연하게 게시글 하나 삭제하면 모든게 전부 삭제되는 기적을 본 적이 있었습니다.하지만 이러한 경험이 있기에 이런글까지 쓰게되지 않았나 싶습니다.그러기에 필요하신분들은 이론적인 개념을 공부하시고 JPA가 궁금하신분들은 우선 만들어보시는것이 더 좋지 않을까 생각합니다.

Ref : 김영한님 - JPA 기본편