TroubleShooting & Study/SpringBoot

[JPA] 연관된 테이블 삭제에 대한 고민(feat. cascade)

DH_0518 2024. 7. 2. 18:34

현재 회원탈퇴 플로우는 다음과 같다

  1. 유저 회원탈퇴시 특정 기간(7일)동안 계정을 비활성화
  2. user 관련된 shorts, comment, follow 등 모든 엔티티를 soft delete처리
  3. 비활성화된 계정에 유저가 접근시, id와 pw 확인해서 계정을 다시 활성화(모든 soft delete 처리된 엔티티를 복구)
  4. 매일 자정 batch를 돌려서 deletedAt이 LocalDataTime.now()와 7일 이상 차이난다면 물리 삭제 진행

 

이제 물리적 삭제를 진행하는 방법을 선택해야한다. 처음에는 CascadeType.ALL 밖에 몰랐는데, 현재 테이블끼리 양방향 매핑이 되어있지 않은 상태이고, 양방향 매핑을 걸어주더라도 성능저하와 순환 참조 문제를 마주할 수 있다는 문제점이 존재한다.

따라서 단방향 매핑에서 해결할 방법을 찾던 도중 @OnDelete 어노테이션을 알게되어서 장단점을 비교해보려 한다.

 

 

 

 

 

 

CascadType.ALL, OnDelete, Query

 

 

 

1:N(부모:자식) 관계에서 동작 방식과 조건은 다음과 같다

  • CascadeType.ALL
    • 동작: JPA에 의해 처리, 부모 엔티티 삭제시 자식 엔티티도 삭제됨
    • 조건: relation, 양방향 매핑 필요
    • 장점: 따로 삭제 로직을 작성할 필요 없이 두 엔티티의 생명주기를 일치시켜 무결성을 보장할 수 있다
    • 단점: relation 필수, 자식 N개 삭제시 N개의 쿼리가 생성, 양방향 매핑 필수, bulk 연산 불가능
  • OnDelete
    • 동작: DDL에 의해 DB자체에 on delete cascade 제약조건이 걸림(즉, db에 의해 처리)
    • 조건: relation 필요
    • 장점: 자식 N개 삭제시에도 JPA 상에서는 한 개의 쿼리만 생성, 단방향 매핑만으로도 해결 가능, bulk 연산 가능
    • 단점: relation 필수, db에 의존성이 생김(확장에 제약이 걸림), 테이블을 새로 생성해야 적용이 됨
  • 개별 Query
    • 동작: 삭제가 필요한 시점에 각각 개별로 쿼리를 날려준다
    • 조건: X
    • 장점: relation이 필요 없음, 확장에 열려있음
    • 단점: 삭제 로직을 하나씩 다 구현해야한다, 무결성을 보장하지 못한다

위의 장,단점을 토대로 Untitled에 맞는 방법을 선택해보자

 

 

 

 

Choice

물리적 삭제가 진행되는건 Batch에서 7일이 지난 시점이다. 여기서 여러 명의 User가 특정되고, 그 User와 1:N이나 1:1 관계인 여러 테이블을 삭제해야 한다. 삭제해야할 테이블 중, 연관된 테이블들을 정리해보자

 

  • Users 테이블과 관계
1:N 관계, relation (O) 유저 관심사 리스트, 반려동물
1:1 관계,  relation (O) 소셜 로그인, 프라이버시 설정
1:N 관계, relation (X) 유저 알림, 유저 쇼츠, 댓글
1:1 관계,  relation (X) 유저 알림설정, 팔로우, 유저 쇼츠

 

  • UserShorts 테이블과 관계
1:N 관계, relation (O) 유저쇼츠태그, 북마크 리스트, 쇼츠 좋아요
1:1 관계,  relation (O)  
1:N 관계, relation (X)  
1:1 관계,  relation (X) 쇼츠

 

  • CommentDetails 테이블과의 관계
1:N 관계, relation (O) 댓글 좋아요(양방향), 언급된 유저(양방향)
1:1 관계,  relation (O) 댓글, 대댓글
1:N 관계, relation (X)  
1:1 관계,  relation (X)  

 

 

 먼저, 모든 댓글에서 두 개의 테이블을 제외한 모든 테이블은 양방향 매핑 관계가 아니다. 따라서 CascadeType.ALL을 사용하려면 추가적인 작업이 필요하다.

 

 다음으로 relation 유무를 살펴봐야 하는데, 설계 당시 fk를 걸어주기 싫어서 아예 relation을 걸어주지 않았다. 물론 지금 생각해보면 JoinColumn에서 ConstraintMode.NO_CONSTRAINT를 사용해주면 되지만, 그 당시에는 이 방법을 몰랐기에 따로 관계를 설정하지 않는 방법 밖에는 없었다.

 

 마지막으로 고려해야할 사항은 bulk 연산이다. batch를 돌리면서 조건에 맞는 user를 모두 찾아오고, 그 유저가 작성한 모든 게시글이나 댓글 등을 삭제해야 하는데, 이 과정에서 bulk delete가 필요하다. 따라서 bulk 연산을 지원하지 않는 CascadeType.ALL은 사용이 불가능하다.

 

 모두 고려해봤을 때, Untitled 서비스는 규모도 그렇게 크지 않고 relation도 걸려있기에 @OnDelete를 사용하는게 좋은 선택인 것 같다.

하지만 언제 fk를 제거하여 사용할 지도 모르고, 여러 테이블이 관계를 맺고있어서 delete 순서가 꼬여버린다면 고아 객체가 발생하여 무결성이 깨질 가능성이 높다는 단점이 크게 와닿아서 그냥 필요할 때 직접 query를 날리기로 했다.

 

 

 

 

여담으로, 아직 지식과 경험이 부족해서 그런거겠지만 DB 관련해서 새로운 기술을 적용할 때, "편리하지만 항상 내가 의도한대로 동작할까?" 라는 의구심이 생기기 시작하면 그 방식을 사용하지는 못할 것 같다.. 얼른 경험을 더 쌓아서 빠르고 정확한 판단을 내릴 수 있도록 성장하고싶다.

 

 

 

 

 

 

 

 

 

 

 

 

 

Reference

 

JPA cascade = CascadeType.REMOVE와 @OnDelete(action = OnDeleteAction.CASCADE)의 차이

게시판 API 만들기 시리즈를 연재하다가, 연관 관계에 있는 엔티티를 연쇄적으로 제거하기 위해 사용되는 위 두 가지 방식의 차이점이 궁금해졌고, 이에 대해 간단히 정리해보았습니다. * 주관적

kukekyakya.tistory.com