jpa 에서 orphanRemoval 옵션을 조심해서 사용해야하는 이유

Brunch Kim·2024년 12월 27일
post-thumbnail

안녕하세요!🙂😊
오늘은 JPA 중에서도 orphanRemoval 옵션에 대해서 집중적으로 알아보려합니다.
orphanRemoval = true는 JPA에서 부모 엔티티와 연관이 끊어진(고아가 된) 자식 엔티티를 자동으로 삭제해주는 옵션입니다.
편리하지만 잘못 사용하면 예상치 못한 삭제가 발생할 수 있는데요.
이것을 이해하려면 실수할 수 있는 대표적인 사례와 함께 작동 원리를 올바르게 이해할 필요가 있습니다.

참고로, 본 내용에서 다루는 Hibernate 버전은 5.x 입니다.


orphanRemoval = true의 동작 원리

  • 부모-자식 관계에서 자식 객체가 부모와의 연관이 끊어지면(컬렉션에서 제거되거나 연관 필드가 null이 되면) 해당 자식 객체를 데이터베이스에서 자동으로 삭제합니다.
  • 이 동작은 CascadeType.REMOVE와 비슷하지만, 차이점은 부모 엔티티를 삭제하지 않아도 작동한다는 점입니다.

주의해야 하는 이유

  1. 예상치 못한 삭제 발생

    • 자식 객체를 단순히 부모 컬렉션에서 제거하거나, 부모와 연관을 끊었을 뿐인데 Hard 또는 Soft 형태의 삭제가 수행됩니다. (Hard = DELETE 발생, Soft = UPDATE 를 통한 is_delete 컬럼 true 처리)
    • 자식 객체를 재사용하려 했는데 삭제되어 데이터 무결성이 깨질 수 있습니다.
  2. 성능 문제

    • 많은 자식 객체가 있을 경우, 연관 관계 변경으로 인해 대량의 DELETE 쿼리가 발생하여 성능 저하가 일어날 수 있습니다.

예시) 쉽게 이해하기 쉬운 orphanRemoval = true 조건으로 인해 엔티티가 삭제가 되는 사례

다음과 같이 Child 엔티티에 다음과 같이 @SQLDelete@Where를 설정했다고 가정합니다.

@Entity
@SQLDelete(sql = "UPDATE child SET is_deleted = true WHERE id = ?")
@Where(clause = "is_deleted = false")
public class Child {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    private boolean isDeleted = false;

    @ManyToOne
    private Parent parent;

    // Getters and Setters
}

그리고 Parent 엔티티를 구현하여 Child 엔티티와 연관관계를 설정합니다.

@Entity
public class Parent {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(mappedBy = "parent", cascade = CascadeType.REMOVE, orphanRemoval = true)
    private List<Child> children = new ArrayList<>();

    public void addChild(Child child) {
        child.setParent(this);
        this.children.add(child);
    }

    public void removeChild(Child child) {
        this.children.remove(child);
        child.setParent(null);
    }
}

ParentChild 엔티티를 다음과 같이 처리합니다.

// Parent와 Child를 저장
Parent parent = new Parent();
parent.setName("Parent 1");

Child child1 = new Child();
child1.setName("Child 1");
parent.addChild(child1);

em.persist(parent);
em.flush(); // DB에 저장

// Parent에서 Child 제거
parent.removeChild(child1); // 고아로 간주됨
em.flush(); // orphanRemoval로 인해 Child 삭제 트리거

동작 분석을 해보면 다음과 같습니다.

  1. parent.removeChild(child1);

    • parent.getChildren().remove(child1)로 Parent와 Child의 관계가 끊깁니다.
    • orphanRemoval = true에 의해 Hibernate는 child1을 고아로 간주합니다.
  2. em.flush();

    • Hibernate는 고아로 간주된 child1을 삭제하려고 합니다.
    • @SQLDelete가 설정되어 있다면 Soft Delete(UPDATE 쿼리)가 실행되고 그렇지 않은 경우 Hard Delete 가 수행됩니다. 위 예제에서는 Soft Delete 가 수행됩니다.

트러블 슈팅

만약 @SQLDelete 를 통해 Soft Delete(UPDATE 쿼리)를 설정했음에도 불구하고 Hard Delete 가 발생했다면?

  • 방안1) Hibernate의 특정 버전(특히 5.x 계열)에서는 @SQLDelete와 orphanRemoval을 함께 사용할 때 동작이 충돌하는 사례가 있었습니다. 6.x 등 최신버전으로의 업데이트가 필요합니다.
  • 방안2) child.setDeleted(true); 와 같이 명시적으로 Soft Delete 를 수행합니다.

참고

profile
브런치 즐기는 여유있는 날

0개의 댓글