해당 포스팅에서는 상품 옵션 기능을 구현하고 기존의 상품 수정 메소드에 상품 옵션을 저장하는 과정에서 발생한 orphanRemoval 관련 문제를 해결한 과정에 대해서 정리해 보고자 한다.
상품 옵션 기능을 구현하고 상품 수정 시 Product의 productOptions 필드에 상품의 옵션 값을 업데이트하는 과정에서 오류가 발생했다. 디버깅을 통해 확인해 본 결과 주어진 예외 메시지를 통해 orphanRemoval 관련된 문제임을 확인할 수 있었다.
예외 메시지는 다음과 같다.
org.springframework.orm.jpa.JpaSystemException: A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance: com.windsome.entity.product.Product.productOptions; nested exception is org.hibernate.HibernateException: A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance: com.windsome.entity.product.Product.productOptions
오류가 발생한 코드는 다음과 같다.
public void updateProduct(ProductFormDTO productFormDto, Category category, List<ProductOption> productOptionList) {
this.name = productFormDto.getName();
this.price = productFormDto.getPrice();
this.inventory = productFormDto.getInventory();
this.productDetail = productFormDto.getProductDetail();
this.productSellStatus = productFormDto.getProductSellStatus();
this.discount = productFormDto.getDiscount();
this.category = category;
this.productOptions = productOptionList; // 이 부분에서 오류 발생
}
먼저 Product와 ProductOption은 1:N 연관 관계를 맺고 있으며, 연관 관계의 주인은 ProductOption이고, Product에서 ProductOption에 대한 orphanRemoval 속성을 true로 설정해 놓은 상태이다.
public class Product {
...
@OneToMany(mappedBy = "product", cascade = CascadeType.ALL, orphanRemoval = true)
private List<ProductOption> productOptions = new ArrayList<>();
...
}
public class ProductOption {
...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id")
private Product product;
...
}
해당 오류는 부모 엔티티의 컬렉션에서 orphanRemoval 속성이 설정된 상태에서 해당 컬렉션의 요소를 변경할 때 발생한다. 즉, 변경된 요소들이 더 이상 참조되지 않으면서 영속성 컨텍스트에서 관리되지 않게 되면 오류가 발생하는 것이다.
해당 오류를 해결하기 위해서는 기존 컬렉션의 요소를 삭제하고 새로운 요소를 추가하여 컬렉션을 업데이트해야 한다. 이를 위해 clear() 메소드를 사용하여 컬렉션을 비우고, addAll() 메소드를 활용하여 새로운 요소들을 추가했다.
public void updateProduct(ProductFormDTO productFormDto, Category category, List<ProductOption> productOptionList) {
this.name = productFormDto.getName();
this.price = productFormDto.getPrice();
this.inventory = productFormDto.getInventory();
this.productDetail = productFormDto.getProductDetail();
this.productSellStatus = productFormDto.getProductSellStatus();
this.discount = productFormDto.getDiscount();
this.category = category;
this.productOptions.clear(); // 추가한 코드
this.productOptions.addAll(productOptionList); // 추가한 코드
}