이전 포스트에서 다루었던 CascadeType.REMOVE 와 orphanRemoval = true 옵션이 각각 고아객체를 어떻게 처리하는지 알아보고자 한다.
🤔 고아객체
부모 엔티티와 연관관계가 끊어진 자식 엔티티
- 부모가 제거될 때, 부모와 연관되어 있는 모든 자식 엔티티들은 고아객체가 된다.
- 부모 엔티티와 자식 엔티티 사이의 연관관계를 삭제할 때, 해당 자식 엔티티는 고아객체가 된다.
공통 예제 코드
CascadeType.REMOVE 와 orphanRemoval = true 옵션이 각각 고아객체를 어떻게 처리하는지 알아보기 위하여, Review 와 ReviewImage 엔티티를 바탕으로 예제 코드를 작성해 보았다.
먼저 두 옵션에 대한 차이점을 제외하고, 공통되는 사항은 아래와 같다.
- Review(One) 과 ReviewImage(Many) 는 다대일 양방향 관계이다.
- 연관관계의 주인은 외래키(reivew_image_id)를 관리하는 Review 이다.
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Review {
@Id
@Column(name = "REVIEW_ID")
private Long id;
@Column(columnDefinition = "TEXT")
private String content;
@OneToMany(mappedBy = "review", cascade = CascadeType.PERSIST)
private List<ReviewImage> reviewImages = new ArrayList<>();
public Review(Long id, String content) {
this.id = id;
this.content = content;
}
}
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class ReviewImage {
@Id
@Column(name = "REVIEW_IMAGE_ID")
private Long id;
private String imageUrl;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "review_id")
private Review review;
public ReviewImage(Long id, String imageUrl) {
this.id = id;
this.imageUrl = imageUrl;
}
public void setReview(Review review) {
// 기존 리뷰와의 연관관계 제거
if (this.review != null) {
this.review.getReviewImages().remove(this);
}
// 새로운 연관관계 설정
this.review = review;
if (review != null) {
review.getReviewImages().add(this);
}
}
}
테스트 코드
두 옵션 테스트를 위해 레포지토리 유닛 테스트를 위해 사용되는 @DataJpaTest 어노테이션을 사용해 주었다.
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY)
@DataJpaTest
class JpaTest {
@Autowired
private EntityManager entityManager;
@BeforeEach
public void initTest() {
Review review = new Review(0L, "리뷰 내용");
entityManager.persist(review);
ReviewImage reviewImage1 = new ReviewImage(0L, "이미지 url");
ReviewImage reviewImage2 = new ReviewImage(1L, "이미지 url");
// 연관관계의 상위 값 설정
reviewImage1.setReview(review);
reviewImage2.setReview(review);
// 부모인 review에 걸려있는 CascadeType.PERSIST 로 인하여 영속성 전이
// entityManager.persist(reviewImage1);
// entityManager.persist(reviewImage2);
// 영속성 컨텍스트의 변경 내용을 DB에 반영
entityManager.flush();
}
}
- @BeforeEach 를 사용하여 각 테스트에 필요한 데이터를 사전해 추가해 준다.
- 부모(Review) 엔티티에 설정해 둔 CascadeType.PERSIST 옵션으로 인해, Review 엔티티를 영속화하면 자식 엔티티인 ReviewImage 엔티티[reviewImage1, reviewImage2]도 같이 영속화된다.
- flush 메서드를 통해 영속성 컨텍스트의 변경 내용을 데이터베이스에 반영한다.
insert into review (content,review_id) values (?,?)
insert into review_image (image_url,review_id,review_image_id) values (?,?,?)
insert into review_image (image_url,review_id,review_image_id) values (?,?,?)
🌏 CascadeType.REMOVE
부모 엔티티가 삭제되면 자식 엔티티도 삭제된다. (부모가 자식의 생명 주기 관리)
부모 엔티티가 자식 엔티티 사이의 연관관계를 제거해도, 자식 엔티티는 삭제되지 않고 그대로 데이터베이스에 남아있다.
기존 코드의 부모 엔티티(Review)에 CascadeType.REMOVE 옵션을 추가해 보았다.
...
public class Review {
...
@OneToMany(mappedBy = "review", cascade = {CascadeType.REMOVE, CascadeType.PERSIST})
private List<ReviewImage> reviewImages = new ArrayList<>();
// constructor
}
1️⃣ 부모 엔티티 삭제
@DisplayName("부모 엔티티(Review)를 삭제하는 경우")
@Test
void cascadeType_REMOVE_Parent() {
// when
Review review = entityManager.find(Review.class, 0L);
entityManager.remove(review); // 부모 엔티티 삭제
entityManager.flush();
// then
List<Review> reviewList = entityManager.createQuery("select r from Review r", Review.class).getResultList();
Assertions.assertEquals(0, reviewList.size());
List<ReviewImage> reviewImageList = entityManager.createQuery("select i from ReviewImage i", ReviewImage.class).getResultList();
Assertions.assertEquals(0, reviewImageList.size());
}
위 테스트에서 entityManager.remove 메서드를 실행하면 아래와 같은 SQL문이 실행된다.
delete from review_image where review_image_id=?
delete from review_image where review_image_id=?
delete from review where review_id=?
부모 엔티티인 Review만 삭제했는데 자식 엔티티인 ReviewImage 삭제까지 총 3번의 DELETE 쿼리가 실행되는 것을 확인할 수 있다.
2️⃣ 부모 엔티티와 자식 엔티티 사이의 연관관계 제거
@DisplayName("고아객체 - 부모 엔티티(Review)에서 자식 엔티티(ReviewImage)와 연관관계를 끊는 경우")
@Test
void cascadeType_REMOVE_Persistence_Remove() {
// when
Review review = entityManager.find(Review.class, 0L);
review.getReviewImages().get(0).setReview(null);
entityManager.flush();
// then
List<Review> reviewList = entityManager.createQuery("select r from Review r", Review.class).getResultList();
Assertions.assertEquals(1, reviewList.size());
List<ReviewImage> reviewImageList = entityManager.createQuery("select i from ReviewImage i", ReviewImage.class).getResultList();
Assertions.assertEquals(2, reviewImageList.size());
}
위 테스트에서는 reviewImage 중 한 개는 setRemove 메서드를 통해 부모 엔티티인 Review와의 연관관계를 끊어 고아객체가 되었다.
update review_image set image_url=?,review_id=? where review_image_id=?
따로 remove 메서드를 실행하지 않았기 때문에 UPDATE 쿼리만 발생하고 DELETE 쿼리는 실행되지 않는다.
🌏 orphanRemoval = true
부모 엔티티가 삭제되면 자식 엔티티도 삭제된다. (부모가 자식의 생명 주기 관리)
부모 엔티티가 자식 엔티티 사이의 연관관계를 제거하면, 자식 엔티티는 고아 객체로 취급되어 데이터베이스에서 삭제된다.
기존 코드의 부모 엔티티(Review)에 orphanRemoval = true 옵션을 추가해 보았다.
...
public class Review {
...
@OneToMany(mappedBy = "review", cascade = CascadeType.PERSIST, orphanRemoval = true)
private List<ReviewImage> reviewImages = new ArrayList<>();
// constructor
}
1️⃣ 부모 엔티티 삭제
@DisplayName("부모 엔티티(Review)를 삭제하는 경우")
@Test
void orphanRemoval_true_Parent() {
// when
Review revuew = entityManager.find(Review.class, 0L);
entityManager.remove(revuew);
entityManager.flush();
// then
List<Review> reviewList = entityManager.createQuery("select r from Review r", Review.class).getResultList();
Assertions.assertEquals(0, reviewList.size());
List<ReviewImage> reviewImageList = entityManager.createQuery("select i from ReviewImage i", ReviewImage.class).getResultList();
Assertions.assertEquals(0, reviewImageList.size());
}
위 테스트에서 entityManager.remove 메서드를 실행하면 아래와 같은 SQL문이 실행된다.
delete from review_image where review_image_id=?
delete from review_image where review_image_id=?
delete from review where review_id=?
부모 엔티티인 Review만 삭제해도 이와 연관된 자식 엔티티인 [reviewImage1, reviewImage2] 삭제까지 총 3번의 DELETE 쿼리가 실행되는 것을 확인할 수 있다.
2️⃣ 부모 엔티티와 자식 엔티티 사이의 연관관계 제거
@DisplayName("고아객체 - 부모 엔티티(Review)에서 자식 엔티티(ReviewImage)와 연관관계를 끊는 경우")
@Test
void orphanRemoval_true_Persistence_Remove() {
// when
Review review = entityManager.find(Review.class, 0L);
review.getReviewImages().get(0).setReview(null);
entityManager.flush();
// then
List<Review> reviewList = entityManager.createQuery("select r from Review r", Review.class).getResultList();
Assertions.assertEquals(1, reviewList.size());
List<ReviewImage> reviewImageList = entityManager.createQuery("select i from ReviewImage i", ReviewImage.class).getResultList();
Assertions.assertEquals(1, reviewImageList.size());
}
위 테스트에서는 reviewImage 중 한 개는 setRemove 메서드를 통해 부모 엔티티인 Review와의 연관관계를 끊어 고아객체가 되었다.
그 이후 remove 메서드를 따로 실행하지 않았는데도 실행된 SQL문을 보면 DELETE 문이 한 번 실행된 것을 확인할 수 있다.
delete from review where review_id=?
이는 고아객체(부모객체가 null)가 된 RemoveImage 엔티티가 자동으로 삭제된 것이다.
3️⃣ 부모 엔티티와 자식 엔티티 사이의 연관관계 변경 시
@DisplayName("자식 엔티티의 연관관계 변경 시")
@Test
void change_persistence_child() {
// given
Review review = new Review(1L, "팀2");
entityManager.persist(review);
// when
ReviewImage reviewImage1 = entityManager.find(ReviewImage.class, 0L);
reviewImage1.setReview(review);
entityManager.flush();
// then
Review review1 = entityManager.createQuery("select r from Review r where r.id = 0", Review.class).getSingleResult();
Assertions.assertEquals(1L, review1.getReviewImages().get(0).getId());
Review review2 = entityManager.createQuery("select r from Review r where r.id = 1", Review.class).getSingleResult();
Assertions.assertEquals(0L, review2.getReviewImages().get(0).getId());
List<ReviewImage> reviewImageList = entityManager.createQuery("select i from ReviewImage i", ReviewImage.class).getResultList();
Assertions.assertEquals(2, reviewImageList.size());
}
insert into review (content,review_id) values (?,?)
update review_image set image_url=?,review_id=? where review_image_id=?
부모 객체를 null로 설정해 연관관계 제거를 하지 않고 다른 Review 엔티티로 변경할 경우, 자식 엔티티는 새로운 부모 엔티티와의 연관관계를 가져 삭제되는 엔티티가 생기지 않게 된다.
🌏 CascadeType.REMOVE vs orphanRemoval = true
- 부모 엔티티 삭제
- CascadeType.REMOVE 와 orphanRemoval = true 옵션 모두 자식 엔티티도 함께 삭제
- 부모 엔티티와 자식 엔티티 사이의 연관관계 제거
- CascadeType.REMOVE 옵션은 외래키 값만 변경된다.
- orphanRemoval = true 옵션은 부모 객체를 잃은 자식 엔티티가 고아객체로 취급되어 데이터베이스에서 삭제
- 부모 엔티티와 자식 엔티티 사이의 연관관계 변경
- CascadeType.REMOVE 와 orphanRemoval = true 옵션 모두 외래키 값만 변경
🌏 주의점
CascadeType.REMOVE 옵션과 orphanRemoval = true 옵션 모두 자식 엔티티에 딱 하나의 부모 엔티티와 연관되어 있는 경우에만 사용해야 한다. 자식 엔티티를 삭제할 상황이 아니어도, 연관되어 있는 1개의 부모 엔티티를 삭제하거나 연관관계를 제거하면 자식 엔티티가 삭제될 수 있기 때문이다.
'Back-end' 카테고리의 다른 글
[Spring] Meta Annotation이란? (@Target, @Retention 등) (0) | 2024.04.29 |
---|---|
[JPA] @SQLDelete와 영속성 컨텍스트 (0) | 2024.04.22 |
[JPA] Cascade 영속성 전이 (1) | 2024.04.19 |
[JPA] EntityManager 핵심 기능 (0) | 2024.04.19 |
[Spring] Spring REST Docs 권한 에러 (2) | 2024.02.29 |