JUN0.DEV
JUN0.DEV

@OnDelete와 JPA Cascade 선택 기준 정리

Published on
  • avatarJunyoung Yang

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

하지만 JPA를 쓰는 상황에서는 DB가 직접 지운 데이터와 영속성 컨텍스트가 알고 있는 상태가 달라질 수 있었다. 이 글은 단순히 연관 데이터를 삭제하는 방법이 아니라, 삭제 책임을 DB에 둘지 JPA에 둘지 판단했던 기록이다.

처음 보인 문제

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

@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가 관리해 일관성을 유지하기 좋다.양방향 관계와 컬렉션 관리 비용이 생긴다.

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

마무리

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

정리한 기준은 아래와 같다.

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

이번 문제는 cascade 설정 하나의 차이라기보다, DB와 ORM 중 어느 레이어가 상태 변화를 책임질지 정하는 문제였다.