본문 바로가기

JPA

영속성 전이(CASCADE)와 고아객체

영속성 전이란?

Shop-Item이라는 엔티티가 관계를 맺고 있다고 해보자!

일반적으로 Item은 Shop이 없다면 Item 자체만으로는 의미를 가지지 못한다.
이런 경우 Shop을 제거하면 Item도 제거되어야 한다.

영속성전이는 이런 경우 필요한 기능으로,
우리가 어떤 엔티티에 대해 명령을 내렸을 때, 이 명령이 관련된 엔티티로 전파되는 것이다.

고아객체란?

부모엔티티와 연관관계가 끊어진 자식 엔티티를 고아객체라고 부른다.
JPA에는 이러한 고아객체를 자동으로 삭제해주는 기능이 있다.


이번 글에서 cascade와 고아객체를 자동으로 제거해주는 기능에 대해서 알아보자!


github 전체코드 주소(branch: blog/cascade)

CASCADE

Parent.java

package hellojpa;

import lombok.Data;

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

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

    private String name;

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

 

Child.java

package hellojpa;

import lombok.Data;

import javax.persistence.*;

@Data
@Entity
public class Child {
    @Id @GeneratedValue
    private Long id;

    private String name;

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

 

JpaMain.java - Parent 저장 로직

// child1, 2 생성
Child child1 = new Child();
child1.setName("child1");

Child child2 = new Child();
child2.setName("child2");

// parent1 생성
Parent parent1 = new Parent();
parent1.setName("parent1");
parent1.getChildren().add(child1);
parent1.getChildren().add(child2);

child1.setParent(parent1);
child2.setParent(parent1);
em.persist(parent1);
//em.remove(parent1);

JpaMain.java를 보면 parent1만 persist하고, child1과 2는 persist하고 있지 않다.
하지만 cascade 옵션에 의해서 persist가 전이되어서 childrent List의 Item인
chlid1과 child2도 persist되어 DB에 저장되는 것을 확인할 수 있다.

remove() 주석인 채로 실행

이렇게 cascade는 특정엔티티에 대한 작업을 연관엔티티로 전이시킨다.
즉, 특정엔티티를 영속상태로 만들 때 관련엔티티도 영속상태로 만들어주며,
특정엔티티를 제거할 때 관련엔티티도 제거해준다.
(가장 하단에 remove 메서드의 주석을 제거 후 실행해보면 위의 설명처럼
테이블의 데이터가 모두 삭제되는 것을 볼 수 있다.)

remove() 주석 해제 후 실행

cascade에서 자주 사용하는 option

  • CascadeType.ALL: 모두 적용
  • CascadeType.PERSIST: 부모엔티티를 영속화할 때 자식엔티티도 영속해주는 기능 활성화
  • CascadeType.REMOVE: 부모엔티티 제거 시  자식엔티티도 제거해주는 기능 활성화
  • 이외에도 여러가지 옵션이 있다! 한 번 공부해보자~~

cascade는 특정엔티티와 관련엔티티의 라이프사이클이 같을 때만 사용해야 한다.

라이프사이클 예시1

게시글 사진 table  → 게시판 table만 FK로 참조 시 게시판 table에 cascade 옵션을 사용해도 괜찮다.
게시판 데이터 삭제 후 cascade에 의해 해당 게시판 글을 참조하던 사진들도 같이 삭제될 것이다.
이상할 것이 없다. 이는 게시글 사진의 라이프사이클과 게시글의 라이프사이클이 같기때문이다.

 

라이프사이클 예시2

결제 table  → 회원 table, 결제 table  → 상품 table을 FK로 참조 시 cascade 옵션을 고려 후 사용해야 한다
이 경우 상품 table에 cascade를 걸어놓는다고 하면 굉장히 이상한 것을 알 수 있다.
만약에 상품을 삭제한다고 하면, 결제 table에서 삭제된 상품을 참조하고 있던 데이터들이 다 삭제된다.
그러면 회원의 입장에서 자신의 결제내역이 날아가고 확인할 수 없게 되는 것이다.

하지만 회원 table에 cascade를 걸어놓는다면 어느 정도 들어맞는 것을 확인할 수 있는데,
회원의 탈퇴 후 회원 데이터를 삭제할 경우, 이 회원을 참조하고 있던 결제 데이터들도 전부 날아가게 된다.
데이터는 날아갔지만 회원의 결제내역 관점에서만 본다면 전혀 문제될 게 없다!
왜냐하면 회원이 탈퇴했기때문에 해당 결제내역을 확인할 사람도 없기때문이다.
즉, 회원과 결제의 라이프사이클이 같기에 cascade 옵션을 사용할 수 있다.
(하지만 매출내역을 고려한다면, 매출이 부정확하게 잡힐 수 있기에 당연히 사용해서는 안 되며, 예시로만 생각하자.)


라이프사이클 예시3

하지만 라이프사이클이 같아도 cascade를 고려해야 하는 상황이 있다.
그건 바로 다른 테이블에서 cascade로 관리하려는 테이블을 참조하는가 여부이다.
ex) 결제 table → 상품 table이고, 상품 table → 상점 table이라고 하자!
상점과 상품의 라이프사이클은 일치하는 것 같아보인다.
(상점이 문을 닫으면, 상품들이 없어지는 것은 라이프사이클 측면에서는 문제될 게 없다.)
하.지.만 결제 table에서 상품 table을 참조하고 있기때문에 상품이 지워지면 결제 data가 영향을 받는다.
(즉, 상점을 지워서 상품이 지워지고, 상품이 지워지면 결제도 영향을 받게 된다.)
하지만 상품과 결제는 라이프사이클이 같지 않다. 따라서 여기서는 cascade를 도입하는 것은 좋은 방법이 아니다.
(만약 결제, 상품, 상점 table까지 3개 테이블의 라이프사이클이 일치한다면 도입하는 것 자체는 문제가 되지 않는다고 생각한다.)

 

결론적으로 cascade를 도입할 때 가장 먼저 고려해야 할 사항은 라이프사이클이며,
라이프사이클이 같아도, 연관테이블이 다른 테이블에 의해 참조되는 경우 도입을 고려해봐야 한다.

고아객체 제거

Parent.java - chlidren

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

JpaMain.java

// child1, 2 생성
Child child1 = new Child();
child1.setName("child1");

Child child2 = new Child();
child2.setName("child2");

// parent1 생성
Parent parent1 = new Parent();
parent1.setName("parent1");
parent1.getChildren().add(child1);
parent1.getChildren().add(child2);

child1.setParent(parent1);
child2.setParent(parent1);
em.persist(parent1);
//em.remove(parent1);

parent1.getChildren().remove(child1);

DB 결과

JPA에는 부모엔티티와 연관관계가 끊어진 자식 엔티티(고아객체)를 자동으로 삭제해주는 기능이 있다.
orphanRemoval을 true로 하면 고아객체를 자동으로 삭제해주는 기능이 활성화된다.
parent1을 참조하고 있는 child1을 List에서 제거하면 JPA는 child1이 parent와 연관관계가 끊어졌다고 판단하고, 
DB에 delete query를 날려서 삭제해준다.

이는 부모엔티티 자체를 remove시킬 때도 마찬가지이다.

(부모엔티티 제거 → 자식엔티티들은 고아객체가 됨 → orphanRemoval 옵션에 의해 자동으로 삭제)


cascade에서도 부모엔티티를 제거하는 경우 이 제거 명령을 자식엔티티로 전파시켜 객체들을 전부 제거해주는 기능은
orphanRemoval과 공통적이다.
하지만 orphanRemoval처럼 부분적으로 원하는 객체만 제거하는 것은 불가능하다!

 


위 코드들을 통해 Child 클래스의 객체들을 직접 persist하거나 remove하지 않았지만,
DB에 저장되고 삭제되는 것을 살펴볼 수 있었다.
이처럼 고아객체와 CASCADE를 모두 활용하면 부모엔티티를 통해 자식 엔티티의 생명주기를 관리하는 것이 가능해진다.

 

참고강의: 배달의 민족 개발팀장 김영한 강사님의 JPA 강의

'JPA' 카테고리의 다른 글

@MappedSuperclass  (0) 2021.08.19
단방향 연관관계  (0) 2021.03.09
기본키 매핑  (0) 2021.02.15
DB 스키마 자동 생성  (0) 2021.02.14
객체와 Table 매핑  (0) 2021.02.14