JPA Cascade 란?
- 어떤 JPA 엔티티는 다른 엔티티의 존재에 깊게 연관되어 있기도 합니다, 가장 대표적인 예시로는
“댓글” , “게시물”이 있으며, 이 둘은 즉 “댓글”은 “게시물”이 없다면 존재하는 의의가 없기 떄문입니다 - 게시판 어플리케이션을 만들어 본다고 가정하며, 게시물을 삭제하는 비즈니스 로직은 어떻게 작성 할 수 있을까요?
// PostService.java
@Transactional
public void deletePost(Long postId) {
Post post = postRepository.findById(postId);
List<Comment> comments = post.getComments();
commentRepository.deleteAll(comments);
postRepository.delete(post);
}
- 삭제시킬 post를 가져온다
- 받아온 post의 댓글을 가져온다
- 모든 댓글을 삭제한다
- 게시글을 삭제한다
- 와 같은 흐름으로 처리될 수 있습니다, 하지만 우리는 “개발자” 이기때문에, 이 방법에대한 문제점을 직면하게됩니다
- 게시물을 삭제할때 댓글 삭제 로직을 일일히 작성해야할까? (어차피 게시글을 삭제하면, 댓글들은 삭제되는것이 당연한 수순이니)
- 실수로 댓글 로직삭제를 깜박하면?
- 더 좋은방법이 있나?
- 이러한 문제점들을 해결하기 위하여, “JPA Cascade”가 등장하였습니다, JPA Cascade를 활용하면
”어떤 엔티티와 다른 엔티티가 밀접한 연관성이 있을때”에 대한 “관리”가 매우 쉬워집니다 - 즉 A라는 엔티티를 수정했고, B라는 엔티티는 A라는 엔티티와의 “연관성”이 있을때, B라는 엔티티에서도 수정가능하게 할 수 있는 뜻입니다
Cascade는 “폭포수가 흐르다” 라는 뜻을 내포하고있다
JPA Cascade Type
JPA에서는 총 6개의 Cascade Type을 지원하는데, 코드와함께 살펴봅시다
상단에서 보았던 게시물,댓글 환경을 예시로 살펴볼것이며, Post Entity의 ? 부분을 바꾸어보며 실습을 해보자
Post Entity
@Entity public class Post { @Id @GeneratedValue private Long id; private String title; @OneToMany(mappedBy = "post", cascade = ?) private List<Comment> comments = new ArrayList<>(); // 생성자 생략 public void addComment(Comment comment) { comments.add(comment); } ... }
Comment Entity
@Entity public class Comment { @Id @GeneratedValue private Long id; private String value; @ManyToOne @JoinColumn(name = "post_id") private Post post; ... }
Persist (영속화)
Entity를 영속화 할때 연관 엔티티도 같이 영속화 시키는 옵션이다
Post post = new Post(); Comment comment = new Comment(); post.addComment(comment); entityManger.persist(post); // post, comment 둘 다 영속화
Merge (병합)
Entity 상태를 병합 할때 연관 엔티티도 같이 병합하는 옵션이다
Post post = new Post("this is post"); Comment comment = new Comment("this is comment"); post.addComment(comment); entityManager.persist(post); entityManager.persist(comment); entityManager.flush(); entityManager.clear(); // 영속성 컨텍스트로부터 분리 post.setTitle("this is changed post"); comment.setValue("this is changed comment"); entityManager.merge(post); // post, comment 모두 변경사항 반영
Remove (제거)
Entity를 제거할때 연관 엔티티도 같이 제거되는 옵션이다
Post post = new Post(); Comment comment = new Comment(); post.addComment(comment); entityManager.persist(post); entityManager.persist(comment); entityManager.remove(post); // post, comment 모두 삭제
Refresh (새로고침)
Entity를 새로고침할때, 연관된 엔티티들도 함께 새로고치는 옵션이다
여기서 “새로고침” 이라는 뜻은, DB로부터 실제 레코드 값을 즉시 로딩하여, 덮어씌운다는 의미
레코드 : 하나의 row
Post post = new Post("this is post"); Comment comment = new Comment("this is comment"); post.addComment(comment); entityManager.persist(post); entityManager.persist(comment); entityManager.flush(); // 데이터베이스에 반영 post.setTitle("this is changed post"); comment.setValue("this is changed comment"); entityManager.refresh(post); // 데이터베이스로부터 post, comment의 원본 값 즉시 로딩 System.out.println(post.getTitle()); // this is post System.out.println(comment.getValue()); // this is comment
Detach(준영속화)
엔티티를 영속성 컨텍스트로부터 분리하면 연관된 엔티티들도 분리되는 옵션이다
Post post = new Post("this is post"); Comment comment = new Comment("this is comment"); post.addComment(comment); entityManager.persist(post); entityManager.persist(comment); entityManager.detach(post); // post, comment 모두 영속성 컨텍스트로부터 분리됨
All (모든 옵션)
- 위에 언급한 모든 Cascade Type 옵션 적용
JPA Cascade가 위험한 이유
- 앞서 보았듯이, Cascade의 개념자체는 어렵지않다, 단순히 연관된 엔티티들에게도 동일하게 적용시킬것인가 정도이다
- 그렇다면 어떤 연유에서 JPA Cascade 가 위험하다고 하는걸까?
- 우리는 JPA Cascade 옵션을 통하여, 연관된 엔티티들의 데이터를 수정,제거 하는등의 작업을 편하게 할 수 있었다
- 하지만 반대로 생각하면 이는 내가 하나의 엔티티를 삭제해도, 그와 연관된 데이터들이 전부 삭제되버리기때문에 충분히 위험 할 수 있다
참조 무결성 제약조건 위반 가능성
- CascadeType.REMOVE or CascadeType.ALL 의 경우, 연관된 엔티티들을 전부 삭제시켜, 참조 무결성 제약조건을 위반 할 수 있습니다
참조 무결성 제약조건 이란, 관계형 데이터베이스 (RDB)에서 릴레이션은 참조할 수 없는 외래 키를 가져서는 안된다는 조건이다
참조 무결성 제약조건의 대한 추가설명 코드
Parent Entity
@Entity public class Parent { @Id @GeneratedValue private Long id; @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true) private List<Child> children = new ArrayList<>(); // getters and setters }
Chiled Entity
@Entity public class Child { @Id @GeneratedValue private Long id; @ManyToOne @JoinColumn(name = "parent_id") private Parent parent; // getters and setters }
AutherEntity
@Entity public class AnotherEntity { @Id @GeneratedValue private Long id; @OneToOne @JoinColumn(name = "child_id") private Child child; // getters and setters }
현재 Parent와 Child는 서로 양방향 다대일관계가 설정되어있으며
Child와 Auther는 Auther → Child(Auther는 Child를 참조한다) 로
단방향 일대일 관계가 설정되어있다여기서 만약 Parent를 삭제할경우, Child들도 같이 삭제되는데
여기서 Child에게 단방향 매핑관계가 설정되어있던 Child가 삭제되면서 Auther 참조할 Child를 잃게되어, “고아”가 되는 상황을 읽컫는다
이런 상황을 “참조 무결성 위반” 이라고 한다
다른 예시를 보면서, 다시한번 이해해 보도록 하자
Post
@Entity public class Post { @Id @GeneratedValue private Long id; private String title; @OneToMany(mappedBy = "post") private List<Comment> comments = new ArrayList<>(); // 생성자 생략 public void addComment(Comment comment) { comments.add(comment); } ... }
comment
// Comment.java @Entity public class Comment { @ManyToOne(cascade = CascadeType.REMOVE) @JoinColumn(name = "post_id") private Post post; // ...생략 }
코드
@Test void remove_bad_case() { Post post = new Post(); Comment comment1 = new Comment(); Comment comment2 = new Comment(); comment1.setPost(post); comment2.setPost(post); postRepository.save(post); commentRepository.save(comment1); commentRepository.save(comment2); commentRepository.delete(comment1); // 관련된 엔티티(post)도 삭제 assertThatThrownBy(() -> entityManager.flush()) .isInstanceOf(PersistenceException.class); // comment2 가 참조하는 post 엔티티가 삭제되었으므로 외래키 관련 예외 발생 // 만약 외래키 제약조건이 없다면 예외가 발생하지 않음 }
흐름
post,comment1,comment2
객체생성post
안에comment1,comment2
데이터 넣기- 세개다 저장
- 이후에
commetn1
만 삭제- 여기서 문제발생
- 현재
comment
에는Cascade.All
옵션이 걸려져있어, 관련된 엔티티가 전부 동일하게 반영되는데, 여기서comment1
을 삭제할시,comment1
과 연관되던post
도 삭제되고, 이에따라post
를 참조하면comment2
는 참조할post
가 사라져 고아가되어 문제가 발생되는것이다
양방향 연관관계 매핑시 충돌 가능성
Comment(N),Post(1)가 다대일 관계로 구성되어있고, 연관관계의 주인은 Commnet(N) 라고 지정
이 경우 Cascade 옵션에 post에 persist를 지정하면 문제가 생길 수 있습니다
post
// Post.java @Entity public class Post { @OneToMany(mappedBy = "post", cascade = CascadeType.PERSIST) private List<Comment> comments = new ArrayList<>(); }
comment
// Comment.java @Entity public class Comment { @ManyToOne(cascade = CascadeType.REMOVE) @JoinColumn(name = "post_id") private Post post; // ...생략 }
코드
@Test void bidirectional_bad_case() { Post post = new Post(); Comment comment1 = new Comment(); Comment comment2 = new Comment(); post.addComment(comment1); post.addComment(comment2); commentRepository.delete(comment1); postRepository.save(post); assertThat(commentRepository.existsById(comment1.getId())).isTrue(); }
흐름
- post객체 생성후, comment1,comment2 집어넣음
- post 객체에 comment,comment2를 추가함
commentRepository.delete(comment1);
JPA의 delete메서드가 호출되며, comment1 객체를 1차캐시에서찾고, DB에 select 쿼리를 날려 조회해보고 있으면, 영속성 컨텍스트에 저장한다. 그리고 그 데이터의 상태를 MANAGED → REMOVE 상태로 전환한다- 그다음 postRepository의 save (persist)가 적용되면서 영속성 컨텍스트안에 있던 데이터의 상태들이 전부 MANAGED (영속)로 바뀌며 전부 저장된다
- 따라서 아무런 데이터의 삭제가 일어나지않고 , comment1,comment2 전부 저장된다
이처럼 영속화에대한 관리 지점이 두곳이면 데이터값을 예측할 수 없는 문제가 발생합니다.
”영속성 전이(cascade) 는 관리하는 부모가 단 하나일때 사용해야한다” 라는 주장이 나온것도 비슷한 맥락입니다
“영속성 전이 (cascade)에 대한 문제는 아래의 예시를 통해 더욱 명확히 알 수 있다
시나리오 설명
- 상황 설정:
A
,B
,C
라는 세 부모 엔티티가 있고, 이들 각각이D
라는 자식 엔티티와 일대일(@OneToOne
) 매핑 관계를 가지고 있습니다.A
엔티티는CascadeType.REMOVE
를 사용하여D
엔티티에 대해 삭제 연산을 수행할 수 있습니다.B
엔티티는CascadeType.PERSIST
를 사용하여D
엔티티를 저장할 수 있습니다.C
엔티티는A
와 마찬가지로CascadeType.REMOVE
를 사용합니다.
- 작동 시나리오:
A
엔티티에 의해D
엔티티가REMOVE
상태로 변경되어 삭제됩니다. 이 작업은 영속성 컨텍스트에서D
를 삭제 상태로 전환하고,flush
시점에 실제로 데이터베이스에서 삭제됩니다.- 그러나,
B
엔티티가D
엔티티에 대해CascadeType.PERSIST
를 사용하여save
를 호출하면,D
는 다시PERSIST
상태로 전환됩니다. 이는 영속성 컨텍스트에서D
를 다시 관리 상태(MANAGED
)로 되돌립니다. - 결과적으로
D
는 다시 데이터베이스에INSERT
되거나, 기존 데이터가 업데이트되어 삭제되지 않고 남아 있게 됩니다.
- 결과:
- 이렇게 서로 다른 부모 엔티티가 동일한 자식 엔티티에 대해 상충되는 연산(
REMOVE
와PERSIST
)을 수행할 때, 데이터의 일관성에 문제가 생길 수 있습니다. D
가A
에 의해 삭제되었지만,B
에 의해 다시 저장되면서 의도한 대로 데이터가 삭제되지 않거나 예상치 못한 상태로 남아 있을 수 있습니다.
- 이렇게 서로 다른 부모 엔티티가 동일한 자식 엔티티에 대해 상충되는 연산(
- 상황 설정:
그렇다면 언제사용하는게 좋을까?
- 위의 예제에서처럼 하나의 자식엔티티에대해, 여러 부모의 엔티티와의 매핑이 되어있는건 상관없는데, 전부다 cascade 옵션으로 넣었을경우에 대해, 문제가 생김을 알아보았다, 그럼 언제써야할까?
- JPA Cascade는 엔티티간의 관계가 “명확” 할때 사용하는것을 권장한다
- 예를들어 Post,Comment 같이 Comment의 현재 부모는 Post 엔티티가 하나여서, 사용하기에 “적절” 하다고 판단 할 수 있다.
참조 문헌
https://tecoble.techcourse.co.kr/post/2023-08-14-JPA-Cascade/
'SpringBoot' 카테고리의 다른 글
다양한 상황에서의 DB에 저장하는 시간을 알아보자 (0) | 2025.02.26 |
---|---|
Redis에 엔티티 저장중 생긴 순환참조문제 (0) | 2025.01.23 |
JSP 란? (1) | 2025.01.21 |
디스패처 서블릿(Dispatcher-Servlet) ? (0) | 2025.01.20 |
서블릿 이란? (1) | 2025.01.20 |