기술 스택
- Spring Boot 3.3.5
- JDK 17
- H2 Database
- JPA
개요
JPA를 공부하다가 CascadeType.REMOVE와 orphanRemoval = true의 차이점에 대해 헷갈렸다. 둘다 자식 객체의 생명주기를 관리하는 코드이기 때문이였다. 그래서 테스트를 통해 알아보았다.
엔티티 기본 세팅
먼저 Parent와 Child 클래스를 생성했다. Parent는 @OneToMany, Child는 @ManyToOne으로 양방향 매핑 했다.
// Parent.java
@Entity
@NoArgsConstructor
@Getter
@Setter
public class Parent {
@Id @GeneratedValue
@Column(name = "PARENT_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "parent")
List<Child> children = new ArrayList<>();
// 연관관계 편의 메서드
public void addChild(Child child) {
children.add(child);
child.setParent(this);
}
}
// Child.java
@Entity
@Getter
@Setter
@NoArgsConstructor
public class Child {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne
@JoinColumn(name = "PARENT_ID")
private Parent parent;
}
CASCADE(CASCADE.PERSIST, CASCADE.REMOVE)
JPA에서 CASCADE는 엔티티 간의 관계에서 부모 엔티티의 작업을 자식 엔티티에도 자동으로 전이시키는 기능을 제공한다. 예를 들어 부모 엔티티를 저장하거나 삭제할 때 자식 엔티티도 자동으로 저장하거나 삭제하도록 설정할 수 있다.
CascadeType.PERSIST
- 부모 엔티티가 영속화될 때(persist 호출), 관련된 자식 엔티티도 자동으로 영속화된다.
- 즉, 부모 엔티티를 저장할 때 자식 엔티티들도 자동으로 함께 저장된다.
CascadeType.REMOVE
- 부모 엔티티가 삭제될 때(remove 호출), 관련된 자식 엔티티도 자동으로 삭제된다.
- 부모와 자식 간의 관계가 강하게 묶여 있어서 부모가 사라질 때 자식도 함께 삭제해야 하는 경우 사용된다.
CascadeType.ALL
- 위에서 언급한 모든 CascadeType을 포함하는 옵션으로, 모든 작업(PERSIST, MERGE, REMOVE, REFRESH, DETACH)이 자식 엔티티에도 전이된다.
1. cascade = CascadeType.REMOVE, orphanRemoval = false
먼저 Parent에 casacade 속성을 'ALL', orphanRemoval는 false로 영속성 전이 옵션을 추가했다.
orphanRemoval는 default가 false이므로 생략해도 무방하다.
Parent.java
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = false)
List<Child> children = new ArrayList<>();
cascade를 ALL을 주면 부모가 자식의 생명주기를 관리한다.
하지만 이옵션의 경우 부모 엔티티가 자식 엔테테와의 관계를 제거해도 자식 엔티티는 삭제되지 않고 그대로 남아있다.
먼저 부모 엔티티를 삭제하는 경우를 살펴보겠다.
@Test
@DisplayName("cascade = CascadeType.ALL / orphanRemoval false: 부모 삭제하면 자식도 같이 삭제 된다.")
void test_0(){
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
//given
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent);
em.flush();
em.clear();
Parent findParent = em.find(Parent.class, parent.getId());
//when
em.remove(findParent);
//then
Assertions.assertThat(em.find(Child.class, child1.getId())).isNull();
Assertions.assertThat(em.find(Child.class, child2.getId())).isNull();
tx.commit();
} catch (Exception e) {
tx.rollback();
throw new RuntimeException(e);
}finally {
em.close();
}
}
DELETE 쿼리가 총 세번 나가는 것을 확인할 수 있다. 즉, 부모(Parent) 객체가 삭제될 때 자식(Child)도 영속성 전이로 인해 같이 삭제되는 것을 볼 수 있다.
Hibernate:
/* delete for jpa.practice.v2.Child */
delete
from
child
where
id=?
Hibernate:
/* delete for jpa.practice.v2.Child */
delete
from
child
where
id=?
Hibernate:
/* delete for jpa.practice.v2.Parent */
delete
from
parent
where
parent_id=?
다음으로 부모 엔티티에서 자식 엔티티를 제거하는 경우를 알아보자.
@Test
@DisplayName("cascade = CascadeType.ALL / orphanRemoval false: 자식을 제거해도 DELETE 쿼리가 실행되지 않는다. ")
void test_1(){
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
//given
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent);
em.flush();
em.clear();
Parent findParent = em.find(Parent.class, parent.getId());
//when
findParent.getChildren().remove(0);
em.flush();
em.clear();
//then
Parent afterFindParent = em.find(Parent.class, parent.getId());
Assertions.assertThat(afterFindParent.getChildren()).hasSize(2);
tx.commit();
} catch (Exception e) {
tx.rollback();
throw new RuntimeException(e);
}finally {
em.close();
}
}
SELECT로 조회만 할 뿐 DELETE 쿼리가 전혀 나가지 않는다. cascade.REMOVE 옵션은 부모와 자식의 관계가 끊어졌다고 해서 자식을 삭제하지 않기 때문이다.
Hibernate:
select
c1_0.parent_id,
c1_0.id,
c1_0.name
from
child c1_0
where
c1_0.parent_id=?
2. cascade = CascadeType.PERSIST, orphanRemoval = true
orphanRemoval = true 옵션 또한 부모 엔티티가 삭제되면 자식 엔티티도 삭제된다. 따라서 cascadeType.PERSIST를 함께 사용하면 부모가 자식의 전체 생명주기를 관리한다.
이번에는 Parent 엔티티에 고아 객체 옵션을 추가한다.
@OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST, orphanRemoval = true)
List<Child> children = new ArrayList<>();
이전과 동일하게 부모 엔티티를 삭제하는 경우를 살펴보자.
@Test
@DisplayName("cascade = CascadeType.PERSIST / orphanRemoval true: 부모 삭제하면 자식도 같이 삭제 된다.")
void test_2(){
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
//given
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent);
em.flush();
em.clear();
Parent findParent = em.find(Parent.class, parent.getId());
//when
em.remove(findParent);
//then
Assertions.assertThat(em.find(Parent.class, findParent.getId())).isNull();
Assertions.assertThat(em.find(Child.class, child1.getId())).isNull();
Assertions.assertThat(em.find(Child.class, child2.getId())).isNull();
tx.commit();
} catch (Exception e) {
tx.rollback();
throw new RuntimeException(e);
}finally {
em.close();
}
}
이때도 DELETE 쿼리가 세번 나가는 것을 확인할 수 있다. 즉, 부모가 삭제될 때 자식도 같이 삭제된다.
Hibernate:
/* delete for jpa.practice.v2.Child */
delete
from
child
where
id=?
Hibernate:
/* delete for jpa.practice.v2.Child */
delete
from
child
where
id=?
Hibernate:
/* delete for jpa.practice.v2.Parent */
delete
from
parent
where
parent_id=?
그럼 부모 엔티티에서 자식 엔티티를 제거하면 어떻게 될까?
@Test
@DisplayName("cascade = CascadeType.PERSIST / orphanRemoval true: 자식을 제거하면 DELETE 쿼리가 나간다 .")
void test_3(){
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
tx.begin();
try {
//given
Child child1 = new Child();
Child child2 = new Child();
Parent parent = new Parent();
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent);
em.flush();
em.clear();
Parent findParent = em.find(Parent.class, parent.getId());
//when
findParent.getChildren().remove(0);
em.flush();
em.clear();
//then
Parent afteerFindParent = em.find(Parent.class, parent.getId());
Assertions.assertThat(afteerFindParent.getChildren()).hasSize(1);
tx.commit();
} catch (Exception e) {
tx.rollback();
throw new RuntimeException(e);
}finally {
em.close();
}
}
}
이전과는 다르게 DELETE 쿼리가 한번만 나간다. 고아 객체 옵션은 부모와 자식의 관계가 끊어지면 자식을 삭제하기 때문이다.
Hibernate:
/* delete for jpa.practice.v2.Child */
delete
from
child
where
id=?
정리
- CascadeType.REMOVE / orphanRemoval = false
- 부모 엔티티에서 자식 엔티티를 삭제해도 DELETE 쿼리가 실행되지 않는다.
- CascadeType.PERSIST / orphanRemoval = true
- 부모 엔티티에서 자식 엔티티를 삭제하면 자식 엔티티 DELETE 쿼리가 실행된다.
주의점
고아 객체 속성은 참조가 제거된 엔티니는 다른곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능이다.
즉, 참조하는 곳이 하나일 때 사용해야 한다. 이말은 즉슨 특정 엔티티가 고유로 개인 소유 할 때 사용해야한다.
왜냐하면 자식 엔티티를 삭제할 상황이 아닌데도 참조가 제거됐다고 해서 자식 엔티티를 삭제되는 사고가 발생할 수 있기 때문이다.
'JPA' 카테고리의 다른 글
[JPA] JPA의 타입 (임베디드 타입, 값 타입, 컬렉션 타입)이란? (1) | 2024.12.20 |
---|---|
[JPA] 프록시와 지연로딩, 즉시로딩 (0) | 2024.11.26 |
[JPA] 연관관계 매핑 (0) | 2024.11.25 |
[JPA] 엔티티 매핑 (0) | 2024.11.25 |
[JPA] JPA와 영속성 컨텍스트란? (1) | 2024.11.18 |