JPA - 영속성 전이 (Cascade) / 고아 엔티티 삭제 (OrphanRemoval)

StrayCat·2026년 2월 25일

영속성 전이란?

영속 상태의 Entity에서 수행되는 작업(저장, 삭제 등)이 연관된 Entity까지 자동으로 전파되는 것을 뜻합니다.

쉽게 말해, 부모 Entity에 특정 작업을 수행했을 때 자식 Entity에도 동일한 작업이 자동으로 따라가도록 설정하는 기능입니다.


CASCADE : PERSIST — 연관 Entity 자동 저장

Cascade 적용 전

// Cascade 없이 저장할 때는 각각 save() 호출이 필요합니다
userRepository.save(user);
foodRepository.save(food);
foodRepository.save(food2);

각 Entity를 일일이 save() 해야 영속 상태가 됩니다. 연관된 Entity가 많아질수록 코드가 복잡해지고 누락 위험도 있습니다.

Cascade 적용 후

@Entity
@Getter
@Setter
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;

    // cascade = CascadeType.PERSIST : User 저장 시 foodList의 Food들도 자동 저장
    @OneToMany(mappedBy = "user", cascade = CascadeType.PERSIST)
    private List foodList = new ArrayList<>();

    // 연관관계 편의 메서드
    public void addFoodList(Food food) {
        this.foodList.add(food);
        food.setUser(this); // 외래 키(연관 관계) 설정
    }
}
@Test
void test2() {
    User user = new User();
    user.setName("Ken");

    Food food = new Food();
    food.setName("후라이드 치킨");
    food.setPrice(15000);
    user.addFoodList(food); // 연관관계 설정

    Food food2 = new Food();
    food2.setName("양념 치킨");
    food2.setPrice(20000);
    user.addFoodList(food2);

    // user 하나만 save해도 food, food2까지 자동으로 INSERT됩니다
    userRepository.save(user);
}

CascadeType.PERSIST 덕분에 userRepository.save(user) 한 번으로 연관된 Food Entity들도 모두 INSERT됩니다.


CASCADE : REMOVE — 연관 Entity 자동 삭제

Cascade 없이 삭제

@Test
@Transactional
@Rollback(value = false)
@DisplayName("Ken 탈퇴 (cascade 없음)")
void test3() {
    User user = userRepository.findByName("Ken");

    // 연관된 음식들을 먼저 수동으로 삭제해야 합니다
    foodRepository.deleteAll(user.getFoodList());

    // 그 후 user 삭제
    userRepository.delete(user);
}

외래 키 제약 조건 때문에 자식 Entity(Food)를 먼저 삭제하지 않으면 오류가 발생합니다.

Cascade REMOVE 적용

@OneToMany(mappedBy = "user", cascade = {CascadeType.PERSIST, CascadeType.REMOVE})
private List foodList = new ArrayList<>();
// 옵션은 배열 형태로 여러 개를 함께 지정할 수 있습니다
@Test
@Transactional
@Rollback(value = false)
void test4() {
    User user = userRepository.findByName("Ken");

    // user 하나만 delete해도 연관된 food들이 자동으로 DELETE됩니다
    userRepository.delete(user);
}

CascadeType 종류 요약

옵션설명
PERSIST부모 저장 시 자식도 자동 저장
REMOVE부모 삭제 시 자식도 자동 삭제
MERGE부모 병합(merge) 시 자식도 자동 병합
REFRESH부모 새로고침 시 자식도 자동 새로고침
DETACH부모 준영속 전환 시 자식도 함께 전환
ALL위 모든 옵션 적용

가장 자주 사용하는 조합은 CascadeType.PERSISTCascadeType.REMOVE이며, 둘 다 적용하려면 cascade = CascadeType.ALL로 간단하게 표현하기도 합니다.


고아 Entity 삭제 (orphanRemoval)

REMOVE와의 차이점

CascadeType.REMOVE부모 Entity 자체가 삭제될 때 자식도 함께 삭제됩니다.

반면 연관관계만 제거(부모의 컬렉션에서 자식을 꺼냄)한 경우에는 자식이 삭제되지 않습니다.

@Test
@Transactional
@Rollback(value = false)
@DisplayName("연관관계만 제거 (REMOVE만으로는 삭제 안 됨)")
void test1() {
    User user = userRepository.findByName("Ken");

    // 후라이드 치킨 Entity를 컬렉션에서 제거 (연관관계 끊기)
    Food chicken = null;
    for (Food food : user.getFoodList()) {
        if (food.getName().equals("후라이드 치킨")) {
            chicken = food;
        }
    }

    if (chicken != null) {
        user.getFoodList().remove(chicken); // 컬렉션에서 제거
    }

    // CascadeType.REMOVE만 설정된 경우 DELETE SQL이 수행되지 않습니다
}

이처럼 컬렉션에서 제거된 후 더 이상 어떤 부모에도 속하지 않는 Entity를 고아(Orphan) Entity라고 합니다.

orphanRemoval 설정

@OneToMany(mappedBy = "user", cascade = CascadeType.PERSIST, orphanRemoval = true)
private List foodList = new ArrayList<>();

orphanRemoval = true를 설정하면, 컬렉션에서 제거된 자식 Entity가 자동으로 DELETE됩니다.


REMOVE vs orphanRemoval 비교

CascadeType.REMOVEorphanRemoval = true
부모 삭제 시 자식 자동 삭제OO
컬렉션에서 제거 시 자식 자동 삭제XO
사용 가능한 애너테이션모두 가능@OneToOne, @OneToMany만 가능

주의 사항

orphanRemoval이나 CascadeType.REMOVE를 사용할 때는 삭제 대상 Entity가 다른 곳에서 참조되고 있지는 않은지 반드시 확인해야 합니다.

A와 B가 모두 참조하고 있던 C를, B 삭제 시 cascade로 함께 삭제해버리면 A 입장에서는 참조 대상이 사라져 문제가 발생할 수 있습니다.

orphanRemoval@ManyToOne에는 제공되지 않는 이유도 이 때문입니다. 여러 부모에게 참조될 수 있는 구조에서는 안전하게 고아 처리를 할 수 없기 때문입니다.


기타

  • 본 포스팅에서 다루는 CascadeType, orphanRemoval 설정 방식은 JPA 2.x 기반으로 현재도 Spring Boot 3.x + Hibernate 6.x 환경에서 동일하게 동작합니다. 레거시 프로젝트에서도 이 방식이 기본이기 때문에 알아두시면 좋습니다.

  • 주의: CascadeType.ALL 남용 금지

편의상 CascadeType.ALL을 무조건 붙이는 경우가 있는데, 이는 의도치 않은 대량 삭제나 예기치 않은 연쇄 저장을 일으킬 수 있습니다. 실제 개발 환경에서는 필요한 옵션만 명시적으로 지정하는 것이 권장됩니다.

  • Spring Data JPA에서 소프트 딜리트(Soft Delete) 패턴

연관 Entity를 물리적으로 삭제하는 대신, deleted 플래그 컬럼을 두고 논리적으로만 삭제하는 소프트 딜리트 패턴도 실제 개발 환경에서 자주 사용됩니다. orphanRemoval과 함께 사용하면 충돌이 발생할 수 있으므로, 소프트 딜리트를 도입할 경우 cascade 전략을 신중하게 재검토해야 합니다.

profile
알면 좋은 것보단 잊어버리기 싫은 것들을 기록합니다.

0개의 댓글