Backend/JPA

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

olsohee 2024. 1. 9. 16:27

영속성 전이(cascade)

특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶으면 영속성 전이 기능을 사용하면 된다. 예를 들어 엔티티를 저장하거나 삭제할 때 연관된 엔티티도 함께 저장하거나 삭제하는 경우이다.

 

JPA는 cascade 옵션으로 영속성 전이를 제공한다.

public enum CascadeType {
    ALL, // 모두 적용
    PERSIST, // 양속
    MERGE, // 병힙
    REMOVE, // 삭제
    REFRESH, // REFRESF
    DETACH // DETACH
}

cascade 옵션 적용 전

예를 들어 다음과 같이 부모와 자식이 일대다 관계로 정의되어 있다고 하자.

@Entity
public class Parent {
	
    @Id @GeneratedValue
    private Long id;
    
    @OneToMany(mappedBy = "parent")
    private List<Child> children = new ArrayList<Child>();
}
@Entity
public class Child {
	
    @Id @GeneratedValue
    private Long id;
    
    @ManyToOne
    private Parent parent;
}

cascade 옵션 없이 부모 1명에 자식 2명을 저장한다면 다음과 같은 코드를 작성한다.

Parent parent = new Parent();
em.persist(parent); //부모 저장

Child child1 = new Child();
child1.setParent(parent);
parent.getChildren().add(child1);
em.persist(child1); //자식1 저장

Child child2 = new Child();
child2.setParent(parent);
parent.getChildren().add(child2);
em.persist(child2); //자식2 저장

JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태이어야 한다. 따라서 위 코드를 보면 부모 엔티티를 영속 상태로 만들고 자식 엔티티도 각각 영속 상태로 만든다. 이럴 때 영속성 전이를 사용하면 부모 엔티티만 영속 상태로 만들면 연관된 자식 엔티티까지 한 번에 영속 상태로 만들 수 있다.

cascade 옵션 적용 후: CasecadeType.PERSIST

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

Child child1 = new Child();
child1.setParent(parent);
parent.getChildren().add(child1);

Child child2 = new Child();
child2.setParent(parent);
parent.getChildren().add(child2);

em.persist(parent); //부모 저장 + 연관된 자식들 저장

cascade를 적용하면, em.persist(parent)를 통해 부모만 영속화하면 cascade = CascadeType.PERSIST로 설정한 자식 엔티티까지 영속화해서 저장한다.

 

이때 주의할 점은 영속성 전이는 연관관계를 매핑하는 것과는 관련이 없다는 점이다. 따라서 영속성 전이를 사용하더라도 항상 양방향 연관관계를 추가해야 한다. 위 예제 코드를 보면 양방향 연관관계를 추가한 다음에 영속 상태로 만드는 것을 확인할 수 있다.

cascade 옵션 적용 후: CasecadeType.REMOVE

@Entity
public class Parent {
	
    @Id @GeneratedValue
    private Long id;
    
    @OneToMany(mappedBy = "parent", cascade = CascadeType.REMOVE)
    private List<Child> children = new ArrayList<Child>();
}
Parent findParent = em.find(Parent.class, 1L);
em.remove(findParent);

cascade = CascadeType.REMOVE로 설정하고 em.remove(findParent)로 부모 엔티티만 삭제하면 연관된 자식 엔티티까지 함께 삭제된다. 이때 delete sql이 3번 실행되어 부모 엔티티, 연관된 두개의 엔티티가 데이터베이스에서 삭제된다.

 

만약 cascade = CascadeType.REMOVE를 설정하지 않고 em.remove(findParent)를 실행하면 어떻게 될까? 그러면 부모 엔티티만 삭제된다. 그런데 데이터베이스의 부모 로우를 삭제하는 순간 자식 테이블에 걸려 있는 외래 키 제약조건으로 인해, 데이터베이스에서 외래키 무결성 예외가 발생한다.

고아 객체

JPA는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 제공하는데 이를 고아 객체(ORPHAN) 제거라 한다. 이 기능을 사용하면, 부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 DB에서 자동으로 삭제된다.

@Entity
public class Parent {
	
    @Id @GeneratedValue
    private Long id;
    
    @OneToMany(mappedBy = "parent", orphanRemoval = true)
    private List<Child> children = new ArrayList<Child>();
}
Parent findParent = em.find(Parent.class, 1L);
findParent.getChildren().remove(0); //컬렉션에서 첫 번째 자식을 제거

findParent.getChildren().remove(0)로 첫 번째 자식을 제거하면 delete from child where id = ? sql이 실행된다. 즉 orphanRemoval = true 옵션으로 인해 컬렉션에서 엔티티를 제거하면 데이터베이스의 데이터도 삭제된다. 고아 객체 제거 기능은 영속성 컨텍스트를 플러시할 때 적용된다. 따라서 플러시 시점에 delete sql이 실행된다.

 

정리하자면, 고아 객체 제거는 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능이다. 따라서 이 기능은 참조하는 곳이 하나일 때만 사용해야 한다. 즉 특정 엔티티가 혼자서만 소유하는 엔티티에만 사용해야 한다. 만약 삭제한 엔티티를 다른 곳에서도 참조한다면 문제가 발생할 수 있다. 이런 이유로 orphanRemoval은 @OneToOne, @OneToMany에서만 사용할 수 있다.

영속성 전이 + 고아 객체

CascadeType.ALL + orphanRemoval = true를 동시에 사용하면 어떻게 될까? 두 옵션을 모두 활성화하면 부모 엔티티를 통해서 자식 엔티티의 생명주기를 관리할 수 있다.

 

예를 들어 자식을 저장하려면 부모에만 등록하면 된다. (CASCADE)

Parent parent = em.find(Parent.class, parentId);
parent.addChild(child1);

그리고 자식을 삭제하려면 부모에서 제거하면 된다. (orphanRemoval)

Parent parent = em.find(Parent.class, parentId);
parent.getChildren().remove(removeObject);

CascadeType.REMOVE와 orphanRemoval = true의 차이

앞서 CascadeType.ALL + orphanRemoval = true를 동시에 사용하면 부모 엔티티가 자식의 생명주기를 관리한다고 했다. 그렇다면, CascadeType.ALL만 사용했을 때와 orphanRemoval = true까지 함께 사용했을 때의 차이는 무엇일까? 즉, CascadeType.REMOVE와 orphanRemoval = true의 차이는 무엇일까?

 

헷갈릴 수 있는 개념이지만, cascade 설정과 orphanRemoval 설정이 아예 별개라고 생각하면 된다.

  • CascadeType.ALL
    • DB에서 부모 엔티티를 삭제하면, 자식 엔티티까지 DB에서 삭제된다.
    • parent.getChildrenList().remove(0)의 경우, 자식 리스트의 첫번째 엔티티는 고아객체가 되더라도 DB에서 삭제되지 않으며 DB에서 외래키도 유지된다.
  • CascadeType.ALL + orphanRemoval = true
    • DB에서 부모 엔티티를 삭제하면, 자식 엔티티까지 DB에서 삭제된다.
    • parent.getChildrenList().remove(0)의 경우, 자식 리스트의 첫번째 엔티티는 고아객체가 되고, 애플리케이션 내에서 parent의 자식 리스트에서 제거됨과 동시에 DB 내에서도 삭제된다.

정리

  • cascade 옵션이 활성화되어 있으면, 영속성에 생긴 변화는 무조건 전파된다. 부모이든 자식이든, 단방향이든 양방향이든 상관없이 전파된다.
  • cascade 또는 orphanRemoval=true 옵션이 적용된 엔티티가 다른 엔티티를 참조하는 부모 엔티티이거나 참조되는 자식 엔티티인 경우, cascade 또는 orphanRemoval=true 옵션에 의해 엔티티가 삭제될 때 문제가 발생할 수 있다. 따라서 여러 엔티티들과 참조 관계를 맺고 있는 엔티티에는 CASCADE 옵션과 orphanRemoval=true를 적용하지 말자.
  • 그렇다면 어디 범위까지 cascade와 orphanRemoval=true 옵션을 적용하는 것이 좋을까? 통상적으로 권장되는 cascade의 범위는 완전히 개인 소유하는 엔티티인 경우이다. 예를 들어 게시판과 첨부파일이 있을 때, 첨부파일은 하나의 게시판 엔티티만 참조하므로 개인 소유이다.

Reference

'Backend > JPA' 카테고리의 다른 글

값 타입 컬렉션  (1) 2024.01.13
JPQL의 소개와 기본 문법  (1) 2024.01.10
프록시와 지연 로딩  (1) 2024.01.09
양방향 연관관계 매핑  (0) 2024.01.09
기본 키 매핑  (1) 2024.01.09