2025. 3. 13. 23:54ㆍDB/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);
}
이렇게 실행을 해보겠다.

분명 서비스 코드에서는 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);
}


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 |
|---|