실전예제 5 는 연관관계를 관리하는 방법에 대해 배운 내용을 적용하는 예제이다.
그 방법에는 아래와 같은 내용이 있다.
- 프록시(Proxy)를 이용하는 지연로딩(fetch = FetchType.LAZY) 적용
- 영속성 전이(cascade=CascadeType.ALL)와 고아객체 (orphanRemoval) 적용 (이번 포스팅)
첫번째인 지연로딩에 대한 포스팅은 아래 글에서 볼 수 있다.
https://kth990303.tistory.com/53
오늘은 두 번째 내용인 영속성 전이에 대해 살펴볼 것이다.
인프런 김영한님의 강의를 듣고
스스로 요약 및 복습용으로 기록한 포스팅입니다.
틀린 내용은 댓글로 피드백 부탁드립니다 :)
영속성 전이(Cascade) 사용해보기
영속성 전이란, 자신의 엔티티를 영속시킬 때, 자신과 연관돼있는 엔티티 또한 영속시키는 것을 말한다.
또한, 사람들이 흔히 cascade와 fetch가 함께 쓰여 둘이 상관관계가 있을 것이라 오해하는데, 영속성 전이(cascade)는 지연로딩, 즉시로딩과 같은 연관관계 세팅과 전혀 상관이 없다고 한다.
예제를 통해 살펴보자.
아래와 같이 Parent, Child 엔티티가 있다.
@Entity
public class Parent {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "parent")
private List<Child> children=new ArrayList<>();
public void addChild(Child child){
children.add(child);
child.setParent(this);
}
}
위 코드는 Parent 엔티티이며, child와 연관관계 매핑이 돼있음을 확인할 수 있다.
child의 부모가 parent이므로, addChild 메소드로 설정해줄 수 있게 만들었다.
@Entity
public class Child {
@Id @GeneratedValue
private Long id;
private String name;
@ManyToOne(fetch = FetchType.LAZY)
private Parent parent;
}
위 코드는 Child 엔티티이며, Parent와 연관관계 매핑이 돼있음을 확인할 수 있다.
@ManyToOne 다대일 연관관계 매핑이므로 지연로딩을 위해 fetch=FetchType.LAZY로 설정해주었다.
Parent parent=new Parent();
parent.setName("kth990303");
Child child1=new Child();
child1.setName("kthbaby1");
Child child2=new Child();
child2.setName("kthbaby2");
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent);
em.persist(child1);
em.persist(child2);
tx.commit();
메인 메소드는 위와 같이 작성하였다.
parent의 이름은 kth990303. 내 이름이다ㅋㅋ
child는 두명으로, kthbaby1, kthbaby2 (작명센스가 많이 부족하다...ㅎ)로 작성하였다.
이후 parent.addChild() 메소드로 부모와 자식 관계를 연결해주고,
em.persist로 부모와 자식들을 모두 영속시켜주었다.
결과는 당연히 아래와 같다.
그런데, 어차피 child는 parent에 속해있는 부모와 자식 관계이므로,
parent를 영속시킬 때, children도 자동으로 영속시켜준다면 굉장히 편할 것 같다.
이 때 사용되는 것이 바로 영속성 전이(cascade)이다.
Parent 엔티티에서 영속성 전이를 해주기 위해 아래와 같이 코드를 추가하였다.
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
private List<Child> children=new ArrayList<>();
cascade=CascadeType.ALL을 추가해준 이후, 메인메소드에서 em.persist(parent);만 해주면 child는 persist해주지 않아도 결과는 위 사진과 똑같이 뜨게 된다.
Cascade의 종류
- ALL: 모두 적용. 웬만한 경우에 쓰인다.
- PERSIST: em.persist 외에 다른 경우엔 cascade 적용을 피하고 싶으면 이걸 사용하자.
그 외 REMOVE: 삭제, MERGE: 병합, REFRESH, DETACH는 잘 사용하지 않는다고 한다.
애초에 cascade 자체가 많이 사용되지 않는다 한다.
그 이유는 아래 글을 참고하자.
https://www.inflearn.com/questions/31969
고아 객체
고아 객체란, 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 말한다.
이러한 고아 객체를 자동으로 삭제되게 할 수 있는 방법이 있는데, 바로 orphanRemoval=true이다.
(역시 혹독한 프로그래밍 세계는 다르다...)
위에 cascade를 공부하면서 작성했던 Parent 코드에 아래와 같이 orphanRemoval=true를 추가하기만 하면 된다.
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Child> children=new ArrayList<>();
이후, 고아객체 삭제를 확인해보기 위해 메인 코드를 아래와 같이 추가수정해주자.
Parent parent=new Parent();
parent.setName("kth990303");
Child child1=new Child();
child1.setName("kthbaby1");
Child child2=new Child();
child2.setName("kthbaby2");
parent.addChild(child1);
parent.addChild(child2);
em.persist(parent);
// 쿼리 확인을 위한 db에 정보 넘겨주기
em.flush();
em.clear();
Parent findParent = em.find(Parent.class, parent.getId());
// 첫번째 자식 삭제
findParent.getChildren().remove(0);
tx.commit();
em.flush(), em.clear()로 영속성 컨텍스트를 비워주고 db에 정보를 넘겨주었다.
또한, 첫 번째 자식을 삭제하여 부모와 자식 엔티티의 연관관계를 끊기 위해 remove를 해주었다.
실행결과는 아래와 같다.
Hibernate:
select
parent0_.id as id1_7_0_,
parent0_.name as name2_7_0_
from
Parent parent0_
where
parent0_.id=?
// 지연로딩 덕분에 children 쿼리는 이후에 날아감.
Hibernate:
select
children0_.parent_id as parent_i3_2_0_,
children0_.id as id1_2_0_,
children0_.id as id1_2_1_,
children0_.name as name2_2_1_,
children0_.parent_id as parent_i3_2_1_
from
Child children0_
where
children0_.parent_id=?
// orphan 객체는 제거함을 아래의 delete 쿼리로 확인할 수 있음.
Hibernate:
/* delete hellojpa.Child */ delete
from
Child
where
id=?
정말 냉혹한 프로그래밍의 세계이다. 고아 객체가 되자마자, orphanRemoval=true 효과로 아예 지워짐을 확인할 수 있다.
만약 orphanRemoval=false로 설정하거나, 아예 orphanRemoval을 설정하지 않으면 아예 삭제가 되지 않는다.
CascadeType은 설정해두고, orphanRemoval은 설정해두지 않을 때, child 한명만 삭제하려면 어떻게 해야 할까? 만약 em.remove(child1)이나 em.remove(child2)를 하면 parent, children 모두가 삭제된다.... (모두 삭제되는 것처럼 보인 이유는 rollback 돼서 그렇다ㅋㅋㅋ)
사실 나도 이 부분을 모르겠어서 QNA에 질문을 올려놨다. 아직 답변이 올라오지 않은 상태이기 때문에 답변이 올라오면 추가수정하겠다.
(21.05.15 추가)
바로 em.remove(child1); 을 하면 안된다. 내가 이미 em.flush(); em.clear(); 로 영속성 컨텍스트를 비워놨기 때문에 child 정보가 영속성 컨텍스트에 존재하지 않아 java.lang.IllegalArgumentException: Removing a detached instance 에러가 발생한 것이기 때문이다.
https://okky.kr/article/439344
child1을 삭제하려면 Child findChild = em.find(Child.class, child1.getId());로 영속성 컨텍스트에 다시 저장시킨 후, em.remove(findChild); 를 하면 된다. 그러면 성공적으로 child만 삭제된다.
영속성 컨텍스트 개념 미숙이였던 것이다.
https://www.inflearn.com/questions/213798
또한, orphanRemoval=true는 정말 주의해서 써야 한다.
잘못하다간 예상치 못한 remove를 해버릴 수도 있기 때문이다.
예를 들어, cascade 관계가 설정돼있지 않다 하더라도, orphanremoval=true일 때, parent가 사망 등 이유로 인해 parent를 삭제하려고 parent를 현상태에서 지워버린다면, children은 부모와 연관관계가 끊어진 엔티티들이므로 모조리 다 삭제가 된다.
즉, 영속성 전이 + 고아객체 제거를 함께 사용할 때의 자식 엔티티 생명주기는 부모엔티티 생명주기의 통제를 따른다. 이는 스스로 생명주기를 통제하는 엔티티가 em.persist, em.remove로 추가 및 제거해줄 수 있는 경우와 대비되는 모습을 보인다.
또한, cascade와 마찬가지로 참조하는 곳이 하나일 때만 사용하도록 해야 한다. 즉, 특정 엔티티가 개인 소유할 때에만 사용하도록 하자.
Cascade와 고아객체 난이도가 하다보니까 은근 어렵다.
처음엔 그냥 같이 영속시켜주는 문법인 줄 알고 우습게 봤고,
cascade+orphanremoval=true를 같이 해주는 경우는, 부모랑 자식이 함께 삭제되는 게 당연해서 그러려니 했다.
그러나, 현재(5월 15일 기준) cascade만 설정해줄 때 em.remove(child1);으로 child 혼자만 삭제하려 했을 때, parent와 children 모두가 삭제되는 현상을 발견하였다.
난 분명 child1만 삭제하고 싶은데, orphanremoval=true 외에는 다른 방법이 없는건지 궁금하다. 아예 cascade를 하면 안되는걸까? 삭제가 아니고, 영속성컨텍스트 개념 미숙으로 인한 error 발생으로 rollback이 일어나 db에 아무것도 저장이 되지 않은 것이었다.
좀 더 공부를 해보아야 할 듯하다.
'JAVA > JPA 학습기록' 카테고리의 다른 글
[JPA] 값 타입 컬렉션, 임베디드 타입 (0) | 2021.05.26 |
---|---|
[JPA] CascadeType.ALL 상태에서의 SQL ERROR: 23503 (0) | 2021.05.15 |
[JPA] 실전예제 5_지연로딩(FetchType.lazy) 적용 (0) | 2021.05.15 |
[JPA] 1:N 연관관계 매핑, N:1 연관관계 매핑 (0) | 2021.04.24 |
[JPA] 실전예제1_요구사항 분석과 기본매핑까지 수강했다 (0) | 2021.04.17 |