JPA로 연관관계를 다루다 보면 cascade와 orphanRemoval이라는 옵션을 자주 만나게 된다. 처음엔 둘 다 연관된 엔티티를 함께 처리한다는 비슷한 개념처럼 보였지만, 실제로 사용해보니 각각 다른 목적과 동작 방식을 가지고 있었다.
내가 구현한 시스템에서 OrderItem은 반드시 하나의 Order에 속해야 한다. 주문 없는 주문 상품이란 있을 수 없기 때문이다. 이런 비즈니스 규칙을 코드로 표현하면 다음과 같다.
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id", nullable = false)
private Order order;
nullable = false로 설정했듯이 OrderItem은 Order없이는 존재할 수 없다. 이처럼 부모-자식 간의 생명주기가 강하게 결합된 구조에서는 다음과 같은 질문이 떠오른다.
바로 이런 상황을 해결하기 위해 JPA는 cascade와 orphanRemoval이라는 옵션을 제공한다.
cascade는 부모 엔티티에 수행된 영속성 작업을 자식 엔티티에도 전파하는 기능이다. 쉽게 말해 Order를 저장하며 연관된 OrderItem들도 자동으로 저장되고, Order를 삭제하면 OrderItem들도 함께 삭제되는 방식이다.
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
private List<OrderItem> orderItems = new ArrayList<>();
이렇게 설정하면 다음과 같은 코드가 가능해진다.
Order order = new Order();
order.addOrderItem(new OrderItem(...));
order.addOrderItem(new OrderItem(...));
orderRepository.save(order); // OrderItem들도 자동 저장
OrderItem을 따로 저장하는 코드를 작성할 필요가 없어진다. 마치 Order와 OrderItem이 하나의 덩어리처럼 움직이는 것이다. 이는 단순히 편의를 위한 기능이 아니라, "주문과 주문 상품은 하나의 단위로 관리되어야 한다"는 도메인 규칙을 코드로 표현한 것이다.
orphanRemoval은 조금 다른 관점에서 접근한다. 이 옵션은 부모와의 관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능이다.
예를 들어 주문에서 특정 상품을 제거하고 싶다고 해보자.
Order order = orderRepository.findById(orderId);
OrderItem itemToRemove = order.getOrderItems().get(0);
order.getOrderItems().remove(itemToRemove);
orphanRemoval = true로 설정되어 있다면, 컬렉션에서 제거된 OrderItem은 데이터베이스에서도 자동으로 삭제된다. Order는 여전히 존재하지만, 관계가 끊어진 OrderItem은 고아 객체가 되어 정리되는 것이다.
처음에는 CascadeType.REMOVE 옵션과 똑같은 거 아닌가? 싶었지만, 작동하는 시점이 완전히 다르다.
CascadeType.REMOVE는 부모가 삭제될 때 자식도 함께 삭제되는 반면, orphanRemoval은 부모가 살아있어도 관계만 끊어지면 자식을 삭제한다.
Order-OrderItem처럼 완전한 종속 관계에서는 두 옵션을 함께 사용하는 것이 자연스럽다.
@OneToMany(
mappedBy = "order",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<OrderItem> orderItems = new ArrayList<>();
이렇게 설정하면 다음과 같은 동작이 가능해진다.
Order 저장 시 OrderItem 자동 저장 (cascade PERSIST)Order 삭제 시 OrderItem 자동 삭제 (cascade REMOVE)Order는 유지하면서 특정 OrderItem만 제거 (orphanRemoval)Order 수정 시 OrderItem도 함께 수정 (cascade MERGE)cascade와 orphanRemoval이 제대로 동작하려면 양방향 연관관계를 항상 동기화해야 한다. 이를 위해 연관관계 편의 메서드는 거의 필수이다.
public void addOrderItem(OrderItem item) {
orderItems.add(item);
item.setOrder(this); // 양방향 관계 설정
}
public void removeOrderItem(OrderItem item) {
orderItems.remove(item);
item.setOrder(null); // 관계 제거
}