본문으로 건너뛰기
김신건의 로그

[JPA] CascadeType과 orphanRemoval

· 수정 · 📖 약 3분 · 1,141자/단어 #jpa #hibernate #cascade #orphan-removal #entity-relationship
JPA Cascade, CascadeType, orphanRemoval, 고아 객체 제거, PERSIST cascade, REMOVE cascade, ALL cascade

정의

JPA 의 entity 연산 (persist, merge, remove, refresh, detach) 을 연관 entity 에 자동 전파 하는 메커니즘. @OneToMany(cascade = CascadeType.ALL) 같이 선언.

별개 개념인 orphanRemoval 은 부모 컬렉션에서 자식이 빠지면 자식 자체를 삭제. cascade = REMOVE 와 비슷해 보이지만 트리거 조건이 다르다.

잘못 쓰면:

  • ALL cascade + 양방향 연관 → 의도치 않은 삭제
  • orphanRemoval + 컬렉션 재할당 → 전체 삭제 후 재삽입

CascadeType 6가지

Type전파되는 연산
PERSISTem.persist(parent) 시 child 도 persist
MERGEem.merge(parent) 시 child 도 merge
REMOVEem.remove(parent) 시 child 도 remove
REFRESHem.refresh(parent) 시 child 도 refresh
DETACHem.detach(parent) 시 child 도 detach
ALL위 다섯 + 향후 추가될 모든 cascade

기본은 cascade 없음. 명시적으로 설정해야 전파.

PERSIST cascade 예

@Entity
class Post {
    @Id @GeneratedValue Long id;
    String title;

    @OneToMany(mappedBy = "post", cascade = CascadeType.PERSIST)
    List<Comment> comments = new ArrayList<>();

    public void addComment(Comment c) {
        comments.add(c);
        c.setPost(this);
    }
}

@Entity
class Comment {
    @Id @GeneratedValue Long id;
    String body;
    @ManyToOne Post post;
}
java
@Transactional
public void create() {
  Post p = new Post();
  p.setTitle("Hello");
  p.addComment(new Comment("first"));
  p.addComment(new Comment("second"));

  em.persist(p);   // post + comments 모두 INSERT
  // cascade 없으면 comments 만 따로 persist 해야 함
}
실행된 SQL
insert into post (title, ...) values (?, ...);
insert into comment (post_id, body, ...) values (?, ?, ...);
insert into comment (post_id, body, ...) values (?, ?, ...);

cascade 없으면 TransientObjectException: comment 가 transient 상태인데 post 가 참조한다고 에러.

REMOVE cascade 와 함정

@OneToMany(mappedBy = "post", cascade = CascadeType.REMOVE)
List<Comment> comments;

em.remove(post) → 모든 comments 도 DELETE.

함정: 양방향 + 다른 entity 와 공유되면 의도치 않은 삭제:

@ManyToOne(cascade = CascadeType.REMOVE)   // ❌ 위험
User user;

em.remove(post) 호출 시 그 post 의 user 까지 삭제됨. user 가 다른 post 의 부모일 수도 있는데. @ManyToOne / @ManyToManyREMOVE cascade 는 거의 항상 잘못된 설계.

규칙:

  • @OneToMany, @OneToOne소유자 → 종속물 관계에만 REMOVE 적합
  • @ManyToOne, @ManyToManyREMOVE 금지

orphanRemoval

부모의 컬렉션에서 자식이 제거 되면 자식 자체를 DELETE:

@OneToMany(mappedBy = "post", orphanRemoval = true)
List<Comment> comments;
java
@Transactional
public void deleteFirst(Long postId) {
  Post p = em.find(Post.class, postId);
  Comment first = p.getComments().remove(0);   // 컬렉션에서 빼면
  // orphanRemoval = true 면 DELETE 자동
}
실행된 SQL
select p.* from post p where p.id = ?
select c.* from comment c where c.post_id = ?
delete from comment where id = ?

cascade = REMOVE 와 다른 점:

  • cascade = REMOVE: 부모 자체가 삭제될 때만 트리거
  • orphanRemoval = true: 부모-자식 참조 관계가 끊어질 때마다 트리거 (부모는 살아 있어도)

cascade = ALL, orphanRemoval = true 조합이 가장 흔함: aggregate root 의 종속 entity 처럼 동작.

orphanRemoval 함정: 컬렉션 재할당

// ❌ 위험
post.comments = new ArrayList<>(newComments);

orphanRemoval = true 면 기존 컬렉션의 모든 원소가 orphan 으로 인식 → 전체 DELETE → 새로 INSERT. 변경된 원소만 update 하려는 의도와 다름.

올바른 패턴:

post.comments.clear();
post.comments.addAll(newComments);
// 또는 retainAll / removeAll 로 diff

양방향 연관과 cascade

@Entity
class Post {
    @OneToMany(mappedBy = "post", cascade = ALL, orphanRemoval = true)
    List<Comment> comments = new ArrayList<>();

    public void addComment(Comment c) {
        comments.add(c);
        c.setPost(this);   // 양방향 동기화 필수
    }

    public void removeComment(Comment c) {
        comments.remove(c);
        c.setPost(null);   // orphanRemoval 가 인식할 수 있게
    }
}

addComment / removeComment 같은 편의 메서드 로 양방향 일관성 유지. 직접 양쪽 setter 호출은 누락 위험.

CascadeType.MERGE 의 묘한 동작

merge 는 새 managed 인스턴스를 반환한다. cascade MERGE 가 있으면 자식도 merge 됨:

Post detached = ...;
detached.getComments().add(new Comment("new"));

@Transactional
public void update(Post detached) {
    Post managed = em.merge(detached);     // cascade MERGE 면 comments 도 merge
    // detached.getComments() 의 객체들은 여전히 detached
    // managed.getComments() 의 객체들이 새 managed
}

@Modifying @Query 로 직접 UPDATE 하면 cascade 도 dirty checking 도 안 일어남. 의도적으로 우회할 때만 사용.

종합 비교

시나리오cascadeorphanRemoval
부모 persist 시 자식도 INSERTPERSIST 또는 ALL-
부모 remove 시 자식도 DELETEREMOVE 또는 ALL-
컬렉션에서 빠지면 자식 DELETE-true
부모 = aggregate root, 자식은 독립 의미 없음ALLtrue
외래키만 공유하는 독립 entity (User, Category)(없음)false

시나리오별 선택

시나리오 1: Order ↔ OrderItem (소유 관계)

OrderItem 은 Order 없이는 의미 없음. Order 삭제 → OrderItem 삭제 자연스러움. 컬렉션에서 빼면 DB 에서도 삭제.

@Entity
class Order {
    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
    List<OrderItem> items = new ArrayList<>();
}

cascade = ALL + orphanRemoval = true 조합. DDD aggregate root 패턴.

시나리오 2: Post ↔ Comment (느슨한 소유)

Comment 도 Post 종속이지만 한 번에 삭제하기엔 너무 많을 수 있음 (수십만 댓글). 대용량 환경:

@Entity
class Post {
    @OneToMany(mappedBy = "post", cascade = {PERSIST, MERGE})   // REMOVE 제외
    List<Comment> comments = new ArrayList<>();
}

Post 삭제 시 별도 batch / soft delete 패턴 사용. cascade REMOVE 면 트랜잭션이 거대해짐.

시나리오 3: Post ↔ Tag (다대다, 독립 entity)

Tag 는 여러 Post 가 공유. Post 삭제해도 Tag 살아야:

@Entity
class Post {
    @ManyToMany   // cascade 없음
    @JoinTable(name = "post_tag")
    Set<Tag> tags = new HashSet<>();
}

조인 테이블 row 만 정리됨. Tag 자체는 보존.

시나리오 4: Book ↔ BookCover (1:1 소유)

@Entity
class Book {
    @OneToOne(mappedBy = "book", cascade = ALL, orphanRemoval = true, fetch = LAZY, optional = false)
    BookCover cover;
}

optional = false + bytecode enhancement 로 LAZY 도 작동.

시나리오 5: User ↔ Profile (1:1, 분리 가능)

Profile 이 별도 의미 있으면 cascade 제한적:

@Entity
class User {
    @OneToOne(mappedBy = "user", cascade = PERSIST)   // 자동 저장만, 삭제는 명시적
    Profile profile;
}

시나리오 6: 양방향 동기화 편의 메서드

cascade 가 작동하려면 컬렉션 조작도 일관성 있게:

@Entity
class Order {
    @OneToMany(mappedBy = "order", cascade = ALL, orphanRemoval = true)
    List<OrderItem> items = new ArrayList<>();

    public void addItem(Product product, int qty) {
        OrderItem item = new OrderItem(this, product, qty);
        items.add(item);    // 양방향 동기화
    }

    public void removeItem(OrderItem item) {
        items.remove(item);
        item.detachFromOrder();   // orphan 으로 만들기
    }

    public void clearItems() {
        // ❌ items = new ArrayList<>() → 전체 orphan, 폭탄 INSERT
        // ✓
        items.clear();
    }
}

시나리오 7: 벌크 삭제는 cascade 우회

// JPQL 벌크 삭제는 cascade 무시
em.createQuery("DELETE FROM Order WHERE createdAt < :date")
  .setParameter("date", oneYearAgo)
  .executeUpdate();
// → OrderItem 은 그대로 남음! 외래키 위반.

해결: 자식 먼저 삭제하거나 native SQL + ON DELETE CASCADE 사용.

자주 보는 패턴

Aggregate Root

@Entity
class Order {
    @OneToMany(mappedBy = "order", cascade = ALL, orphanRemoval = true)
    List<OrderItem> items = new ArrayList<>();

    public void addItem(Product p, int qty) {
        items.add(new OrderItem(this, p, qty));
    }

    public void removeItem(OrderItem i) {
        items.remove(i);
    }
}

OrderItem 은 Order 외부에서 의미 없음 → 라이프사이클 일체. DDD aggregate root 패턴과 자연스럽게 매핑.

독립 entity 참조

@Entity
class Post {
    @ManyToOne                              // cascade 없음
    @JoinColumn(name = "category_id")
    Category category;
}

Category 는 Post 와 독립. Post 삭제해도 Category 살아있어야 함 = cascade 없음.

함정과 베스트 프랙티스

  • cascade = ALL@OneToMany, @OneToOne 의 소유자에만
  • @ManyToOne / @ManyToMany 에 REMOVE 절대 금지
  • 양방향 연관은 편의 메서드로 동기화
  • orphanRemoval + 컬렉션 재할당은 전체 삭제 + 재삽입 폭탄
  • @Modifying @Query 는 cascade 우회: 의도가 아니면 사용 금지
  • 벌크 삭제 시 cascade 무시됨: delete from Order JPQL 은 자식 cascade 안 일어남. 직접 자식 먼저 삭제
  • flushclear 사이 cascade 동작: 큰 그래프 persist 시 메모리 주의

관련 위키

이 글의 용어 (5개)
[JPA] Entity Lifecycle Callbacks와 Auditingspring
정의 Entity 의 라이프사이클 (persist / update / load / remove) 특정 시점에 자동 호출되는 메서드. 두 가지 형태: 1. Entity 내부 메서드…
[JPA] FetchType: LAZY vs EAGER, N+1, fetch joinspring
정의 FetchType 은 JPA 가 연관 entity 를 언제 로드할지 결정하는 hint. - : entity 로딩 시 즉시 함께 SELECT - : 실제 사용 시점 (prox…
[JPA] PersistenceContext: 1차 캐시, dirty checking, flushspring
정의 PersistenceContext 는 가 관리하는 entity 의 메모리 저장소 + 변경 추적기. 트랜잭션 안에서만 살아 있다 ( 진입 시 생성, commit/rollbac…
[Spring] Spring Data JPA: Repository, Query Methodsspring
정의 Spring Data JPA는 JPA(Hibernate) 위에 Repository 추상화를 얹어 boilerplate를 제거. 인터페이스만 정의하면 Spring이 구현체 자…
[Spring] Transaction: @Transactional, propagation, isolationspring
정의 Spring 트랜잭션은 AOP 기반 선언형 트랜잭션 관리. 한 줄로 메서드 전체를 트랜잭션 범위로. JDBC, JPA, MongoDB 등 backend에 동일 API. 기본…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기