OndeleteCascade vs deleteAllInBatch - 어느것을 택해야 할까

Kim Dong Kyun·2023년 1월 22일
1

Today I learned

목록 보기
38/43

개요

  • 바로 이전 프로젝트에서 객체의 양방향 설정을 하면서, 부모 객체를 삭제 할 때 자식 객체에 몇개의 delete 쿼리가 발생할까? 는 고민하지 않았습니다.
  • 그저 아~ 그냥 delete하면 자연스럽게 자식 객체도 삭제되고, cascade타입이 All/REMOVE 와 같이 생성과 삭제에 완전히 영속성이 결합된(전이된) 상태면 별 문제가 없겠구나.
  • orphanRemoval = true 와 같은 펑션을 쓰면 알아서 JPA가 삭제해주는구나 라고 생각했습니다.

다음은 제가 실제로 이전 프로젝트에서 썼던 연관관계이고, delete쿼리에 대해서 신경쓰지 않고 설정한 부분입니다.

그리고 아래는 부모 객체인 Memo, 즉 게시글 객체를 삭제하는 매서드입니다.

위와 같은 형태에서는 Memo를 삭제 할 때 연관되어있는 Comment에 N+1번의 delete 쿼리가 나갑니다. 더하여 연관관계 없이 설정해두었던 Like(좋아요)객체들의 삭제 쿼리까지 하면 정말 너무 많은 쿼리가 발생하고 있습니다.

그리고 이 문제의 원인은, 부모 객체를 삭제 할 때 자식 객체를 한번에 삭제하지 못하고 일일이 for문을 돌면서 삭제해야 한다는 한계점 때문이었습니다. JPA에서 기본적으로 제공하는 부분이죠


그러나 실제 서비스를 생각 해 보면 delete쿼리가 몇개 나가는지는 굉장히 중요한 문제입니다.

만약, 게시글 하나에 댓글이 1만개 달려있다면? 게시글에 있는 좋아요는 2만개이고, 댓글의 좋아요 총합은 5만개라면?

  • 위와 같은 경우 일일이 delete 쿼리를 날리면 DB의 부담이 너무 심합니다. 게시글 하나 삭제하기 위해 너무 많은 부담을 져야하죠.

  • 그래서, 이번 프로젝트에는 같은 실수를 반복하지 않고자 delete 쿼리를 최소화하는 방법들을 검색했습니다. 이에 따라 정리합니다.


1. OnDeleteCascade

OnDeleteCascade 는 DB 테이블에서 FK로 연결된 row들을 한번에 지우는 방법입니다. 스프링 JPA 환경에서는 @OnDelete 어노테이션을 통해서 사용 가능합니다.

@OnDelete(action = OnDeleteAction.CASCADE)의 형태로 사용합니다.

위와 같이, FK를 가지는 자식 엔티티에 있는 부모 필드(즉, FK...) 에 설정합니다.

위와 같이 @OnDelete 어노테이션을 붙인다는 것은, 데이터 베이스 단계에서의 관리를 하겠다는 의미입니다. On delete cascade는 DDL(Data Definition Language)이며 테이블간의 연관관계를 DB에서 관리합니다.

장점

위 같은 경우 N+1 문제를 바로 해결 할 수 있습니다. 부모 객체인 Memo가 삭제 될 시에 자식 객체인 Comments 들에 대해서 하나의 쿼리로 삭제가 일어납니다

단점

JPA에서 기본 제공하는 cascade 기능들이 왜 자식 객체들을 하나하나 삭제할까를 생각해보면, 위와 같이 DB단계에서 직접적으로 관여하는 것은 DB의 무결성을 훼손할 가능성이 생깁니다. 즉, ondelete 설정으로 인해 DB에 있는 테이블의 참조 부분까지 모조리 삭제 할 가능성이 생긴다는 것입니다.

위와 같은 형태는 즉 설계자가 삭제의 모든 과정을 신경쓰고 관여해야 한다는 것이고, 제어 관계의 역전(IoC) / 객체 지향의 다형성 등의 원리에 부합되지 않습니다. 우리는 최대한 JPA가 제공하는 기능을 안전하게 사용하는 것이 더 나을 것입니다.


2. deleteAllInBatch

deleteAllInBatch 는 where 조건에 맞는 엔티티들을 한방에 날려주는 쿼리를 작성해줍니다. 사용은 다음과 같이 합니다(예시입니다)

위와 같은 형태로, deleteAllInBatch 로 코멘트들을 삭제하고!
또 다시 사용해서 Memo에 붙은 Likes들을 삭제하고! 와 같은 식입니다.

위 방법보다는 안전해 보이기는 합니다. 그러나 보시는 여러분들도 상당히 불편하실테고, 눈에 보이는 문제보다 쪼오끔 더 문제가 되는 부분이 있습니다.

JPA repository에서 제공하는 delete 매서드들은 기본적으로 delete 쿼리를 날리기 전에 조회를 먼저 실행합니다. 따라서 성능 이슈가 있을 수 있습니다.

그러면 어쩌라는건가요?


3. JPQL(혹은 SQL) 사용해서 직접 지우기

@Modifying 어노테이션과 @Query 어노테이션을 사용해 직접 딜리트 쿼리를 날리는 방법입니다.

이 방법이 제일 많이 쓰는 방법이며, 다음과 같은 장점을 가집니다.

  1. 조회 쿼리가 발생하지 않습니다.
  2. 한번의 쿼리로 벌크 연산을 처리 할 수 있습니다. (이는 update 문에서도 사용 가능하다는 얘기입니다)
public interface CategoryRepository extends JpaRepository<Category, Long> {
    @Query("delete from Category c where c.seller.id = :sellerId")
    @Modifying(clearAutomatically = true)
    void deleteAllBySellerId(@Param("sellerId") Long sellerId);
}

위와 같은 형태로 사용합니다.


결론

deleteAllInBatch / 직접 delete쿼리 날리기 등이 현실적으로 써봄 직 합니다. 그 중에서도 딜리트 쿼리를 직접 날리는 부분을 더 많이 고려해야 할 것입니다.

추가적으로, Batch라는 키워드도 공부해 볼만 한 것 같습니다.

+ 참고자료

https://mingg123.tistory.com/117

0개의 댓글