팀 프로젝트를 하던 중 좋아요 기능을 구현할 때였다. 따로 로직을 구현하여 위시리스트를 만들려고 하던 중, JPA의 CascadeType.REMOVE
기능을 알게 되었다. CascadeType.REMOVE
를 사용하면 해당 엔티티를 삭제할 때 연관된 엔티티도 함께 삭제된다는 것이다. 내가 구현하려는 좋아요 기능에 딱 맞는다고 생각하고 바로 적용했다. 그러나, 바로 오류를 경험했다. 특정 좋아요를 삭제하면 관련된 상품이 함께 사라지는 현상이 발생한 것이다. 곧바로 CascadeType.REMOVE
를 검색해보니, 사용 시 주의해야 한다는 점이 강조되어 있었다. 이 경험을 통해 Cascade 기능을 제대로 이해하고 사용하기로 결심하고 공부를 시작했다.
JPA Cascade는 한 엔티티의 상태 변화가 연관된 다른 엔티티로 전파되도록 하는 설정입니다. CascadeType에는 여러 가지 옵션이 있다
먼저 casecadeType.persist는 부모 엔티티가 영속화 될 때 자식 엔티티도 영속화 된다. 즉, 새로운 부모 엔티티가 생기면 자식 엔티티도 같이 데이터베이스에 저장할고자 할 때 사용한다.
@Entity
public class Parent {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(cascade = CascadeType.PERSIST)
private List<Child> children = new ArrayList<>();
// getters and setters
}
@Entity
public class Child {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// Other fields, getters and setters
}
// 사용 예시
Parent parent = new Parent();
Child child1 = new Child();
Child child2 = new Child();
parent.getChildren().add(child1);
parent.getChildren().add(child2);
entityManager.persist(parent); // 이 시점에 child1과 child2도 자동으로 영속화됨
실제 코드에서는 entityManager는 JPA repository에서 데이터를 넣으면 entityManager.persist이 자동으로 실행된다.
그 다음은 CasecadeType.REMOVE이다.
좋아요 기능으로 예시를 들어보겠다 .
import javax.persistence.*;
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "user", cascade = CascadeType.REMOVE, orphanRemoval = true)
private List<Like> likes = new ArrayList<>();
// getters and setters
}
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "product", cascade = CascadeType.REMOVE, orphanRemoval = true)
private List<Like> likes = new ArrayList<>();
// getters and setters
}
@Entity
public class Like {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
@ManyToOne
@JoinColumn(name = "product_id")
private Product product;
// getters and setters
}
여기서 orphanRemoval = true는 무엇일까? orphanRemoval = true
는 JPA에서 엔티티가 부모 엔티티와의 관계에서 제거될 때 데이터베이스에서도 해당 엔티티를 자동으로 삭제하도록 설정하는 옵션입니다. 이는 CascadeType.REMOVE
와 자주 함께 사용되어, 부모 엔티티에서 자식 엔티티가 삭제되면 데이터베이스에서도 해당 자식 엔티티가 삭제되도록 보장합니다.
orphanRemoval = true
와 CascadeType.REMOVE
의 차이점은 다음과 같습니다:
그렇다면 Casecade를 사용할 떄 무엇을 조심해야하는가?
기본 개념은 크게 어렵지 않다 오히려 JPA가 알아서 삭제해주니 구현해야할 코드가 줄어들어 편리해보인다. 하지만 연관관계 매핑에서 잘못사용하게 되면 의도하지 않은 결과가 나타나다.
CascadeType.REMOVE
혹은 CascadeType.ALL
옵션을 사용하면 엔티티 삭제 시 연관된 엔티티들이 전부 삭제되고 이로 인해 참조 무결성 제약조건을 위반할 가능성이 있다..
예를 들어서 위 좋아요 기능에서
@Test
void remove_bad_case() {
// 새로운 사용자와 상품 생성
User user = new User();
user.setName("John Doe");
Product product = new Product();
product.setName("Awesome Gadget");
// 새로운 좋아요 생성
Like like1 = new Like();
like1.setUser(user);
like1.setProduct(product);
Like like2 = new Like();
like2.setUser(user);
like2.setProduct(product);
// 엔티티 저장
userRepository.save(user);
productRepository.save(product);
likeRepository.save(like1);
likeRepository.save(like2);
// 좋아요 삭제
likeRepository.delete(like1); // 이 시점에 user와 product도 삭제
// flush 시점에 참조 무결성 제약조건 위반 예외 발생
assertThatThrownBy(() -> entityManager.flush())
.isInstanceOf(PersistenceException.class);
// like2가 참조하는 user와 product가 삭제되었으므로 외래키 관련 예외 발생
}
위 예제에서 like1
을 삭제했을 뿐인데, CascadeType.REMOVE
설정 때문에 user
와 product
가 함께 삭제되었습니다. 이로 인해 like2
가 참조하는 user
와 product
가 없어져 참조 무결성 제약조건 위반 예외가 발생하게 됩니다.
잘못된 사용을 피하기 위해 CascadeType.REMOVE
와 orphanRemoval
를 조심스럽게 사용해야 합니다. 좋아요 기능에서는 Like
엔티티만 삭제하고 User
와 Product
는 유지해야 합니다.
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "user", orphanRemoval = true)
private List<Like> likes = new ArrayList<>();
// getters and setters
}
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
@OneToMany(mappedBy = "product", orphanRemoval = true)
private List<Like> likes = new ArrayList<>();
// getters and setters
}
@Entity
public class Like {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
@ManyToOne
@JoinColumn(name = "product_id")
private Product product;
// getters and setters
}
@Test
void remove_good_case() {
// 새로운 사용자와 상품 생성
User user = new User();
user.setName("John Doe");
Product product = new Product();
product.setName("Awesome Gadget");
// 새로운 좋아요 생성
Like like1 = new Like();
like1.setUser(user);
like1.setProduct(product);
Like like2 = new Like();
like2.setUser(user);
like2.setProduct(product);
// 엔티티 저장
userRepository.save(user);
productRepository.save(product);
likeRepository.save(like1);
likeRepository.save(like2);
// 좋아요 삭제
likeRepository.delete(like1);
// flush 시점에서 예외가 발생하지 않음
assertThatNoException().isThrownBy(() -> entityManager.flush());
// user와 product는 여전히 존재
assertThat(userRepository.existsById(user.getId())).isTrue();
assertThat(productRepository.existsById(product.getId())).isTrue();
// like1은 삭제되었고, like2는 여전히 존재
assertThat(likeRepository.existsById(like1.getId())).isFalse();
assertThat(likeRepository.existsById(like2.getId())).isTrue();
}
이 예제에서는 orphanRemoval
을 사용하여 Like
엔티티가 User
나 Product
엔티티와의 관계에서 제거될 때 자동으로 삭제되도록 했습니다. 하지만 CascadeType.REMOVE
를 제거하여 User
와 Product
엔티티가 Like
엔티티 삭제 시 함께 삭제되지 않도록 했습니다.
이렇게 함으로써 참조 무결성 제약조건을 위반하지 않으면서 원하는 기능을 구현할 수 있습니다.
Cascade는 올바르게 사용하면 매우 편리한 기술이다. 그러나 이를 오용하면 데이터의 일관성과 무결성이 손상될 수 있다. 따라서 Cascade를 사용할 때는 항상 엔티티 간의 관계를 명확히 이해하고, 적절한 CascadeType을 선택하며, 필요 시 orphanRemoval
을 활용하여 데이터의 일관성을 유지해야한다.
좋아요 기능과 같은 예제에서는 Like
엔티티만 삭제하고 User
와 Product
엔티티는 유지하는 것이 맞습니다. 이런 경우에는 CascadeType.REMOVE
를 사용하지 않고 orphanRemoval
만 사용하는 것이 바람직합니다. Cascade는 편의성을 높여주는 도구이지만, 사용에 따른 부작용을 항상 염두에 두고 신중하게 적용해야 한다.