[Spring Data JPA] Cascade와 OrphanRemoval
JPA를 사용하여 Entity 클래스를 설계하다보면 @OneToMany
나 @ManyToOne
와 같은 연관 관계 매핑을 주로 사용하게 된다. 이때 cascade 옵션을 사용하여 매핑된 Entity를 함께 관리할 수 있다.
Entity Cascade
Entity cascade는 Entity의 상태 변화를 전파시키는 옵션이다. 매핑되어 있는 Entity에 대해 어느 한 쪽 Entity의 상태가 변경되었을 때 그에 따른 변화를 바인딩된 Entity에게 전파하는 것을 말한다.
다음과 같이 Post Entity와 Comment Entity가 OneToMany 연관 관계를 가진다고 가정해보자.
@Entity
@ToString(exclude = "commentList")
@Getter
public class Post {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL) //cascade 옵션 적용
private List<Comment> commentList = new ArrayList<>();
protected Post() {
}
@Builder
public Post(String title) {
this.title = title;
}
public void addComment(Comment comment) {
this.commentList.add(comment);
}
}
@Entity
@ToString(exclude = "post")
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String content;
@ManyToOne(fetch = FetchType.LAZY)
private Post post;
protected Comment() {
}
@Builder
public Comment(String content, Post post) {
this.content = content;
this.post = post;
}
public Comment(Post post) {
this.post = post;
}
}
Post Entity의 commentList를 보면 cascade 옵션이 적용되어있는 것을 볼 수 있다. 이렇게 되면 Post 객체만 영속화하여도 cascade 옵션으로 인해 영속성 전이가 일어나 commentList에 저장되어 있는 Comment 객체들도 영속화가 된다.
@Test
public void cascadePersistTest() {
// Post entity 생성
Post post = Post.builder().title("Spring Data JPA-Cascade").build();
// Comment entity 생성
Comment comment = Comment.builder().content("good").post(post).build();
Comment comment1 = Comment.builder().content("thank you :)").post(post).build();
// post에 comment 추가
post.addComment(comment);
post.addComment(comment1);
// 영속성 컨텍스트에 post entity 저장
entityManager.persist(post);
// 영속성 컨텍스트를 비움
entityManager.clear();
// post entity 조회
Post readPost = entityManager.find(Post.class, post.getId());
System.out.println(readPost);
}
위의 코드처럼 post만 저장하면 commentList에 포함된 comment도 함께 저장되는 것을 볼 수 있다.
삭제에서도 동일하다.
@Test
public void cascadeRemoveTest() {
// 생략
// 삭제할 post 조회
Post rmPost = entityManager.find(Post.class, post.getId());
// post entity 삭제
entityManager.remove(rmPost);
// commit
entityManager.flush();
}
post entity만 삭제했음에도 post entity와 매핑된 comment entity가 함께 삭제되는 것을 볼 수 있다.
Cascade Type
Cascade type으로는 총 6가지가 있다.
- CascadeType.ALL: 모든 Cascade를 적용
- CascadeType.PERSIST: entity를 영속 상태로 만들면, 매핑된 entity도 함께 영속 상태가 됨
- CascadeType.MERGE: 준영속 상태의 entity를 영속 상태로 만들면, 매핑된 entity도 모두 영속 상태가 됨
- CascadeType.REMOVE: entity를 제거하면 매핑된 entity도 모두 제거
- CascadeType.DETACH: entity를 준영속 상태로 만들면, 매핑된 entity도 준영속 상태가 되어 entity manager가 관리하지 않음
- CascadeType.REFRESH: entity를 다시 읽어올 때, 매핑된 entity도 모두 다시 읽어옴
OrphanRemoval
orphanRemoval 옵션은 말 그대로 연관 관계가 사라진, 고아가 된 entity를 삭제하는 것을 말한다.
- 특정 entity가 단일 소유할 때, 참조하는 곳이 하나일 때 사용
- @OneToOne, @OneToMany만 사용 가능
- ophanRemoval 옵션 사용
- 추가적으로 부모를 제거할 때 자식도 같이 제거된다
위의 Post 클래스의 postList에 orphanRemoval=true를 추가해보자.
//cascade 옵션 적용, orphanRemoval 옵션 적용
@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Comment> commentList = new ArrayList<>();
post의 commentList에서 comment entity를 삭제하면 연관 관계가 사라진 comment entity가 삭제되는 것을 볼 수 있다.
@Test
public void orphanRemovalTest() {
// 생략
// post 조회
post = entityManager.find(Post.class, post.getId());
// post에 매핑된 comment 제거
post.getCommentList().remove(0);
// commit
entityManager.flush();
}
결론
개발을 하다보면 매핑된 entity를 삭제하지 않아 이슈가 발생한 경험이 종종 있다. 이러한 문제점들을 해결하기 위해서 cascade = CascadeType.ALL과 orphanRemoval = true 옵션을 같이 사용해 부모 entity를 통해서 자식 entity의 생명 주기를 관리함으로써 실수를 최소화할 수 있을 것이다.
Reference
https://developer-hm.tistory.com/47