JUN0.DEV
JUN0.DEV

@OnDelete와 JPA Cascade 선택 기준 정리

Published on
  • avatarJunyoung Yang

카카오테크캠퍼스 선물하기 API를 JPA로 옮기던 중, 상품을 삭제하면 연결된 옵션도 함께 삭제해야 하는 요구사항이 있었다. 처음에는 DB에서 cascade delete를 걸면 간단히 끝날 문제라고 생각했다.

하지만 JPA를 쓰는 상황에서는 DB가 직접 지운 데이터와 영속성 컨텍스트가 알고 있는 상태가 달라질 수 있었다. 삭제만 정상적으로 되면 끝이라고 보기에는, 삭제 흐름을 어느 쪽에서 관리할지가 달랐다. 이 글은 단순히 연관 데이터를 삭제하는 방법이 아니라, DB cascade와 JPA cascade 중 어떤 방식이 이 상황에 더 맞는지 비교한 기록이다.

문제 상황

당시 엔티티 관계는 단방향에 가까웠다.

@Entity
public class Option {

    @ManyToOne(fetch = FetchType.LAZY)
    private Product product;
}

OptionProduct를 알고 있었지만, ProductOption 목록을 직접 들고 있지 않았다. 이 구조에서는 상품 삭제 시 옵션까지 함께 삭제하려면 선택지가 생긴다.

  • DB 레벨에서 ON DELETE CASCADE를 사용한다.
  • JPA 연관관계를 양방향으로 바꾸고 cascade, orphanRemoval을 사용한다.
  • 서비스에서 옵션 삭제를 명시적으로 처리한다.

처음에는 단방향 구조를 유지할 수 있는 @OnDelete가 가장 간단해 보였다.

방법 1: @OnDelete

Hibernate의 @OnDelete를 사용하면 외래 키에 DB cascade delete를 걸 수 있다.

@ManyToOne(fetch = FetchType.LAZY)
@OnDelete(action = OnDeleteAction.CASCADE)
private Product product;

이 방식의 장점은 분명했다. ProductOption 컬렉션을 몰라도 되고, DB가 연결된 row 삭제를 처리해준다. 코드도 적고, 기존 단방향 관계를 크게 건드리지 않아도 된다.

하지만 편한 만큼 조심할 부분도 있었다. 실제 삭제를 처리하는 쪽이 JPA가 아니라 DB가 되기 때문이다.

문제가 된 지점

JPA는 영속성 컨텍스트 안에서 엔티티 상태를 추적한다. 그런데 @OnDelete는 Hibernate가 DDL에 cascade 규칙을 만들고, 실제 삭제는 DB가 처리한다.

그러면 같은 트랜잭션 안에서 이런 상태 차이가 생길 수 있다.

  • DB에서는 Option이 이미 삭제됐다.
  • 영속성 컨텍스트에는 해당 Option이 아직 남아 있을 수 있다.
  • 이후 같은 트랜잭션에서 해당 엔티티를 다시 참조하면 예상과 다른 동작이 나올 수 있다.

단순 삭제만 보면 문제가 없어 보여도, 애플리케이션이 알고 있는 상태와 DB가 실제로 처리한 상태가 어긋날 가능성이 있었다. JPA를 사용하는 이유 중 하나가 객체 상태와 변경 감지를 애플리케이션에서 다루기 위해서라면, 이 부분도 함께 봐야 했다.

방법 2: JPA Cascade

최종적으로는 ProductOption 목록을 관리하고, JPA cascade와 orphanRemoval을 사용하는 방향을 선택했다.

@Entity
public class Product {

    @OneToMany(
            mappedBy = "product",
            cascade = CascadeType.ALL,
            orphanRemoval = true
    )
    private List<Option> options = new ArrayList<>();
}

이렇게 하면 상품 삭제 시 JPA가 연결된 옵션 삭제까지 함께 처리한다. 영속성 컨텍스트도 삭제 상태를 같이 보게 되므로, 애플리케이션 입장에서 상태를 맞추기 더 쉬웠다.

물론 비용도 있다. ProductOption 컬렉션을 갖게 되면서 관계가 양방향이 되고, 연관관계 편의 메서드나 컬렉션 관리에 신경 써야 한다. 그래도 이 경우에는 삭제 흐름을 JPA 안에서 처리하는 편이 더 나았다.

선택할 때 본 것

두 방식은 단순히 우열의 문제가 아니라 볼 지점이 다르다.

방식장점주의할 점
@OnDelete단방향 관계 유지가 쉽고 설정이 간단하다.DB가 직접 삭제하므로 영속성 컨텍스트와 상태가 어긋날 수 있다.
JPA Cascade삭제 흐름을 JPA가 함께 처리해 상태를 맞추기 좋다.양방향 관계와 컬렉션 관리 비용이 생긴다.

이번 요구사항에서는 상품과 옵션이 같은 묶음에 가깝고, 옵션의 생명주기가 상품에 종속적이었다. 그래서 삭제 흐름을 JPA가 함께 처리하는 편이 더 적절하다고 봤다.

정리

이 작업 이후로 연관 삭제를 볼 때는 "지워지기만 하면 된다"가 아니라 "삭제된 상태를 어느 쪽에서 관리해야 하는가"를 먼저 확인한다.

확인한 내용은 아래와 같다.

  • 단순 정합성만 필요하고 애플리케이션에서 삭제된 자식을 다시 만질 일이 적다면 DB cascade도 선택지가 될 수 있다.
  • JPA 영속성 컨텍스트 안에서 상태를 맞춰야 하면 JPA cascade를 우선 검토한다.
  • 부모와 자식의 생명주기가 강하게 묶이면 orphanRemoval까지 함께 본다.
  • Hibernate 전용 기능을 쓸 때는 편의성뿐 아니라 이식성과 상태 관리 범위도 확인한다.

이번 문제는 cascade 설정 하나의 차이라기보다, DB와 ORM 중 어느 쪽에서 상태 변화를 다룰지 정하는 문제였다.