[Java] JPA - 영속성 전이: CASCADE

daheenamic·2025년 11월 19일
0

JAVA

목록 보기
40/41

영속성 전이와 고아 객체

영속성 전이 (CASCADE)

영속성 전이는 특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶을 때 사용하는 기능이다. 즉시 로딩, 지연 로딩과는 전혀 관련이 없다. 말 그대로 부모를 저장할 대 관련된 자식을 다 같이 저장하고 싶을 때 사용한다.

예를 들어, 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장하고 싶은 경우에 사용한다.

영속성 전이를 사용하지 않는 경우

다음과 같이 Parent와 Child 엔티티가 설계되어 있다고 가정하자.

@Entity
public class Parent {
    @Id @GeneratedValue
    private Long id;
    
    private String name;
    
    @OneToMany(mappedBy = "parent")
    private List<Child> childList = new ArrayList<>();
    
    // 연관관계 편의 메서드
    public void addChild(Child child) {
        childList.add(child);
        child.setParent(this);
    }
}

@Entity
public class Child {
    @Id @GeneratedValue
    private Long id;
    
    private String name;
    
    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Parent parent;
}

이 상태에서 Parent와 Child를 저장하려면 다음과 같이 세 번 persist()를 호출해야 한다.

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);  // 번거로움

이렇게 하면 좀 귀찮아진다. 이때 사용하는 게 CASCADE이다.

CASCADE 사용하기

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

이렇게 설정하고 실행하면 em.persist(parent)만 해도 child 둘 다 자동으로 persist된다.

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

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

em.persist(parent);  // child1, child2도 자동으로 persist됨

영속성 전의 주의사항

영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없다.
단지 엔티티를 영속화 할 때 연관된 엔티티도 함께 영속화하는 편리함을 제공할 뿐이다.

CASCADE의 종류

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

보통 ALL이나 PERSIST를 많이 사용한다.

CASCADE를 사용해야 하는 경우

그렇다고 모든 일대다 관계에 CASCADE를 거는건 아니다. 하나의 부모가 자식들을 관리할 때만 사용해야 한다.

사용해도 되는 경우:

  • 게시글 1개에 여러 첨부파일이 있는 경우
  • 첨부파일은 해당 게시글에서만 관리하기 때문에 CASCADE를 사용해도 된다.

사용하면 안 되는 경우:

  • 그 파일을 다른 엔티티에서도 관리하고 있다면 사용하면 안된다.
  • 소유자가 하나일 때만 사용해야 한다.

댓글 예시
회원이 게시글에 댓글을 남긴다고 가정할 때, 댓글은 게시글과도 연관되어 있고 회원과도 연관되어 있다. 이런 경우 댓글의 생명주기는 게시글에만 의존하는가? 아니면 회원에도 의존하는가?
일반적으로 댓글은 게시글에 종속되므로 게시글에서만 CASCADE를 사용할 수 있다. 하지만 만약 회원이 탈퇴해도 댓글이 남아있어야 한다면, 댓글은 독립적인 생명주기를 가져야 하므로 CASCADE를 사용하지 않는 것이 좋다.


고아 객체

고아 객체는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 말한다.
JPA는 고아 객체를 자동으로 삭제하는 기능을 제공한다.

고아 객체 제거 사용하기

orphanRemoval = true 옵션을 사용하면 된다.

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

컬렉션에서 제거한다는 의미
Parent의 childList에서 0번 인덱스의 Child를 제거하면, 해당 Child는 더 이상 Parent와 연관관계가 없어진다.
이때 orphanRemoval = true 옵션이 설정되어 있으면, JPA는 이 child를 고아 객체로 판단하고 자동으로 DELETE 쿼리를 실행한다.

실행되는 쿼리:

DELETE FROM CHILD WHERE ID = ?

고아 객체 제거 주의사항

  • 참조가 제거된 엔티티는 다른 곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능이다.
  • 참조하는 곳이 하나일 때만 사용해야 한다.
  • 특정 엔티티가 개인 소유할 때만 사용해야 한다.
  • @OneToOne, @OneToMany만 가능하다.

참고
개념적으로 부모를 제거하면 자식은 고아가 된다. 따라서 고아 객체 제거 기능을 활성화하면, 부모를 제거할 때 자식도 함께 제거된다. 이것은 CascadeType.REMOVE처럼 동작한다.


영속성 전이 + 고아 객체로 생명주기 관리하기

두 옵션을 함께 사용하기

CascadeType.ALL + orphanRemoval=true를 함께 사용하면 부모 엔티티를 통해서 자식의 생명주기를 완전히 관리할 수 있다.

@Entity
public class Parent {
    @OneToMany(mappedBy = "parent", 
               cascade = CascadeType.ALL, 
               orphanRemoval = true)
    private List<Child> childList = new ArrayList<>();
}

생명주기 관리란?

일반적으로 엔티티는 스스로 생명주기를 관리한다.

  • em.persist()로 영속화
  • em.remove()로 제거

하지만 두 옵션을 모두 활성화 한다면 다음과 같이 부모를 통해서 자식의 생명주기를 관리할 수 있다.

// 자식 저장: parent에 추가만 하면 자동 저장 (CASCADE)
parent.addChild(child);
em.persist(parent);  // child도 자동으로 persist

// 자식 삭제: parent에서 제거만 하면 자동 삭제 (orphanRemoval)
parent.getChildren().remove(0);  // child 자동으로 delete

// 부모 삭제: 자식도 함께 삭제 (CASCADE + orphanRemoval)
em.remove(parent);  // 모든 child도 자동으로 delete

CASCADE.ALLorphanRemoval=true의 차이
CASCADE.ALL은 부모의 영속성 상태 변화를 자식에게 전파하는 것이다(저장, 삭제 등). orphanRemoval=true는 부모와의 관계가 끊어진 자식을 자동으로 삭제하는 것이다. CASCADE.ALL만 있으면 컬렉션에서 제거해도 DB에서는 삭제되지 않지만, orphanRemoval=true가 있으면 컬렉션에서 제거하는 순간 DB에서도 삭제된다.

실무 활용

이 패턴은 도메인 주도 설계(DDD)의 Aggregate Root 개념을 구현할 때 유용하다.

Aggregta Root
DDD에서 여러 엔티티를 하나의 집합으로 묶어서 관리할 때, 그 집합을 대표하는 루트 엔티티를 말한다.
예를 들어, 주문(Order)과 주문상품(OrderItem)이 있을 때, Order가 Aggregate Root가 되고, OrderItem의 생명주기는 Order를 통해서만 관리하는 방식이다. 이렇게 하면 리포지토리는 Order만 있으면 되고, OrderItem은 Order를 통해서만 접근하게 된다.

0개의 댓글