[JPA] cascade 에 cascadeType 은 어떤 것이 있을까?

2025. 3. 13. 23:54DB/JPA

개요

오늘 과제 중에 cascade = CascadeType을 지정을 해주었는데 왜 돌아가는 것인지 궁금해서

CascadeType에 대해서 자세하게 조사를 해보았다.

 

일단 과제의 결론을 이야기하면 밑에 생성자에 값을 넣어주는 것을 못찾아서 그랬지만

확실하게 알게 되어서 결과는 좋다. 한잔해

 

 


 

Cascade란?

@OneToMany 나 @ManyToOne 에 옵션으로 줄수 있는 값이면서 Entity의 상태 변화를 전파 시키는 옵션이다.

 

이렇게 말을 하면 뭔가 어려운 듯 하니 풀어서 설명을 해보면

 

1대 다 관계에서 사용을 할 수 있으면서,

 

그 관계에서 하나의 Entity가 값이 변하거나, 삭제 되거나, 기존 값에서 변경이 된다면

 

그 자식의 객체에도 값을 전파 시키는 것이다.

 

 


 

 

CascadeType은 어떤 것이 있는가?

  • CascadeType.ALL
    • ALL은 밑에 있는 것을 전부 포함한 전파를 의미함
  • CascadeType.PERSIST
    • 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장한다.
  • CascadeType.MERGE
    • 부모 엔티티를 병합할 때 자식 엔티티도 병합한다.
  • CascadeType.REMOVE
    • 부모 엔티티를 삭제할 때 자식 엔티티도 삭제된다.
  • CascadeType.DETACH
    • 부모 엔티티를 분리하면 자식 엔티티도 분리된다.
  • CascadeType.REFRESH
    • 부모 엔티티를 새로고침할 때 자식 엔티티도 새로고침된다.

이렇게 총 6가지가 있다. ALL을 제외하고 하나씩 알아보자.(ALL은 가장 마지막에 나온다)

 

 


 

예시 전에 돌리는 코드 확인

기본코드 수정은 간단하게 우리 모두의 친구 chatGPT를 사용해서 만들어보았다.

 

Parent

@Entity
@Getter
@Setter
class Parent {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

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

    public void addChild(Child child) {
        children.add(child);
        child.setParent(this);
    }
}

 

 

Child

@Entity
@Getter
@Setter
class Child {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Parent parent;
}

 

 

Repository

public interface ParentRepository extends JpaRepository<Parent, Long> {}

public interface ChildRepository extends JpaRepository<Child, Long> {}

 

 

Controller

@RestController
@RequestMapping("/parents")
@RequiredArgsConstructor
public class ParentController {
    private final ParentService parentService;

    /** 부모 + 자식 저장 (CascadeType.PERSIST) */
    @PostMapping("/save")
    public String saveParentWithChildren() {
        parentService.saveParentWithChildren();
        return "부모와 자식 저장 완료";
    }

    /** 부모 수정 (CascadeType.MERGE) */
    @PutMapping("/{parentId}")
    public String updateParent(@PathVariable Long parentId) {
        parentService.updateParent(parentId);
        return "부모 및 자식 정보 수정 완료";
    }

    /** 부모 삭제 (CascadeType.REMOVE) */
    @DeleteMapping("/{parentId}")
    public String deleteParent(@PathVariable Long parentId) {
        parentService.deleteParent(parentId);
        return "부모 및 자식 삭제 완료";
    }

    /** 부모 새로고침 (CascadeType.REFRESH) */
    @PutMapping("/refresh/{parentId}")
    public String refreshParent(@PathVariable Long parentId) {
        parentService.refreshParent(parentId);
        return "부모 및 자식 새로고침 완료";
    }

    /** 부모 detach (CascadeType.DETACH) */
    @PutMapping("/detach/{parentId}")
    public String detachParent(@PathVariable Long parentId) {
        parentService.detachParent(parentId);
        return "부모 및 자식 detach 완료";
    }
}

 

 

어떻게 돌아가는지 확인만 해보려고 chatGPT에게 코드를 작성하게 해두었고, 내용은 이제 테스트를 진행을 해보고자 한다.

 

 


예시.1 CascadeType.PERSIST

// ParentService
/** ✅ 부모와 자식 함께 저장 (CascadeType.PERSIST) */
    @Transactional
    public void saveParentWithChildren() {
        Parent parent = new Parent();
        parent.setName("부모");

        Child child1 = new Child();
        child1.setName("자식1");
        parent.addChild(child1);

        Child child2 = new Child();
        child2.setName("자식2");
        parent.addChild(child2);

        // 부모만 저장해도 자식이 자동으로 저장됨
        parentRepository.save(parent);
    }

 

이렇게 실행을 해보겠다.

 

Controller 저장을 실행 시켰다.

 

분명 서비스 코드에서는 parentRepository 에 부모내용만 저장을 하는 모습을 확인할 수 있다.

 

H2 DB에 들어가서 값을 확인을 해보면 부모만 넣었는데, 자식값이 들어간 모습을 확인 할 수 있다.

 


예시.2 CascadeType.MERGE

@Transactional
    public void updateParent(Long parentId) {
        Parent parent = parentRepository.findById(parentId)
                .orElseThrow(() -> new RuntimeException("부모 엔티티가 없습니다."));

        parent.setName("수정된 부모");

        for (Child child : parent.getChildren()) {
            child.setName("수정된 " + child.getName());
        }

        // 부모 저장 시 자식도 함께 병합됨
        parentRepository.save(parent);
    }

Controller updateParent를 실행 시켰다.

 

ParentRepository 에 값을 넣고 있는데 자식 값도 수정이 된 모습을 확인 할 수 있다.

 

 


예시.3 CascadeType.REFRESH

/** ✅ 부모와 자식 새로고침 */
    @Transactional
    public void refreshParent(Long parentId) {
        Parent parent = parentRepository.findById(parentId)
                .orElseThrow(() -> new RuntimeException("부모 엔티티가 없습니다."));

        parent.setName("변경된 부모"); // 부모 이름 변경
        for (Child child : parent.getChildren()) {
            child.setName("변경된 " + child.getName()); // 자식 이름 변경
        }

        entityManager.refresh(parent);  // 새로고침 실행
    }

내용을 수정하여 변경된 부모, 변경된 자식이름으로 바꾸었지만 refresh를 하여 DB에서 다시 불러와진 것을 확인할 수 있었다.

참고로 부모 엔티티만 다시 불러오는 것이 아니라 수정된 자식이름도 다시 불러와서 확인을 할 수 있는 것이다.

 


예시.4 CascadeType.DETACH

 /** ✅ 부모 detach 시 자식도 함께 detach (CascadeType.DETACH) */
    @Transactional
    public void detachParent(Long parentId) {
        Parent parent = parentRepository.findById(parentId)
                .orElseThrow(() -> new RuntimeException("부모 엔티티가 없습니다."));

        entityManager.detach(parent);
    }

 

해당 상태에서 parent의 값을 수정을 하여도 parent는 영속성 상태가 아니기 때문에

값이 DB에 저장이 되지 않게 된다.

 

 


 

예시.5 CascadeType.Remove

/** ✅ 부모 삭제 시 자식도 함께 삭제 (CascadeType.REMOVE) */
@Transactional
public void deleteParent(Long parentId) {
    Parent parent = parentRepository.findById(parentId)
            .orElseThrow(() -> new RuntimeException("부모 엔티티가 없습니다."));

    parentRepository.delete(parent);
}

 

부모 객체만 삭제를 하였는데 자식 객체까지 삭제된 모습을 확인할 수 있다.

 

 

* CascadeType.Remove 와 orphanRemoval = true 의 차이점

CascadeType.Remove => 부모 엔티티가 삭제되면 자식 엔티티도 삭제된다.

즉, 부모가 자식의 삭제 생명 주기를 관리한다.

 

orphanRemoval = true => 부모 엔티티가 삭제되면 자식의 엔티티도 삭제된다

하지만 CascadeType.Remove 와 달리 부모 엔티티가 자식 엔티티와의 관계를 제거하면 자식은 고아로 취급되어

그대로 사라진다.

 


 

마무리

사실 이 모든 테스트는 CascadeType.ALL 로 진행을 하였다.

 

 

사실 실무에서도 거의 사용이 되지 않는다는 점을 잘 알고 있다.

하지만 이런게 왜 사용이 되는지, 어떻게 사용이 되는지 알고 있어야한다고 생각을 하였다.

 

요즘 개발을 하면서 이런 생각이 참으로 중요하다고 생각한다.

 

왜 사용해야되는가?

왜 썼는가?

이런 것을 알고 말을 할 수 있어야된다고 점점 생각을 하게 된다.

 

잘 쓰지 않으면서 작은 것에 큰 배움을 얻었다고 생각한다.

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

[JPA] 소프트딜리트를 구현하자  (6) 2025.07.27