프로젝트가 거의 마무리를 지어가나 싶더니 문제가 발생했다.
회원 탈퇴 기능을 개발할 때에 발생한 문제이다.
아래와 같이 연관관계가 있는 Attribute들에 대해서 Option을 걸었다.
CASCADE
와 orphanRemoval
을 통해 연관 객체를 모두 관리할 계획이었다.
하지만, 이 부분에서 문제가 발생했다.
일반적으로 그냥 내가 쓴 글 정도는 회원 탈퇴가 잘 수행되었다.
회원 객체와 함께 연관된 글도 모두 삭제되었다.
하지만 문제는 다대다 관계에서 발생했다.
현재 프로젝트에서는 찜 기능과 참여 기능이 다대다 관계이다.
다대다 관계를 교차 엔티티를 통해 해소를 해놓은 상태인데,
해당 객체가 지워지지 않으니 아래와 같은 문제가 발생했다.
나는 그래서 아래의 사항들에 대해 고민해보기로 했다.
- 애초에 연관 관계가 제대로 설정이 되어있는가?
CASCADE
와orphanRemoval
Option의 무분별한 사용은 괜찮은가?- 로직을 따로 추가해 교차 엔티티 삭제를 먼저 해주어야 할까?
연관 관계의 설정
현재 아래의 사진과 같이 연관 관계가 설정되어있다.
연관 관계의 주인은 외래키를 관리하는 쪽을 정해주는 것이다.
외래키를 들고 있는 쪽에서 변경, 삭제 등 관리하는 것이 효율적이니,
일대다(다대일) 관계에서 외래키를 가진 다(Many) 쪽으로 주인을 정한다.
다대다 관계를 일대다 - 다대일로 풀어 놓은 상황이니,
연관 관계의 주인은 다(Many) 쪽인 교차 엔티티로 맞게 되어있다.
제대로 짜여진 연관 관계이지만 이로 인해 삭제가 불가능한 것이다.
부모인 Member
이나 GroupBuying
이 먼저 지워질 수 없는 것이다.
FK를 들고 있는 연관 관계의 주인을 먼저 지운 뒤, 부모가 지워져야 한다.
CASCADE와 OrphanRemoval
먼저 CASCADE
와 orphanRemoval
에 대해 정리를 해보자.
- CASCADE : 부모의 영속성이 자식에게도 전이되도록 하는 option.
PERSIST, REMOVE..
등 다양한 모드가 존재한다.- 예를 들어,
REMOVE
는 부모 객체가 삭제되면 자식 객체도 삭제가 된다. - 혹은,
PERSIST
는 부모 객체가 영속되면, 자식 객체도 자동으로 영속된다.- 연관 관계가 있는 부모-자식 객체 중 하나라도 영속화가 안되면 예외가 발생한다.
- orphanRemoval : 고아 객체를 자동으로 삭제해주는 option.
- 부모 객체가 사라져
null
이 된 경우 고아 객체 라고 한다. - 이 경우에, 해당 고아 객체를 자동으로 삭제해준다.
- 부모 객체가 사라져
위의 특징들을 보면 알 수 있듯, 우리는 해당 option들을 통해
부모-자식 객체 간의 생명주기를 동일하게 관리할 수 있는 것이다.
그럼 내가 현재 프로젝트에 적용했듯이, 저렇게 막 걸어도 되는 것일까?
내가 해당 옵션을 걸어놓은 객체들은 현재 다음과 같다.
- 내가 쓴 글 (
Member - Contents
)- Member가 탈퇴하면, Contents도 당연히 사라져야 한다.
- 즉, 생명주기를 동일하게 관리할 필요가 있다.
- 참여하는 글 (
Member - Intersection - Contents
)- 글을 쓴 Member가 탈퇴하면?
- 글이 삭제되는 것이 기본이므로, 교차 객체 또한 모두 지워져야 한다.
- 이 경우엔 관련된 모든 교차 객체가 지워져야 한다.
- 그 말은, 관련된 모든 Member의 List에서도 지워져야 한다는 말이다.
- 참여 중인 Member가 탈퇴하면?
- 글은 그대로 남되, 교차 객체가 삭제되어야 한다.
- 이 경우엔 탈퇴하는 Member와 관련된 교차 객체만 삭제하면 된다.
- 글을 쓴 Member가 탈퇴하면?
- 찜한 글 (
Member - Intersection - Contents
)- 글을 쓴 Member가 탈퇴하면?
- 글이 삭제되는 것이 기본이므로, 교차 객체 또한 모두 지워져야 한다.
- 찜을 한 Member가 탈퇴하면?
- 글은 그대로 남되, 교차 객체가 삭제되어야 한다.
- 이 경우엔 탈퇴하는 Member와 관련된 교차 객체만 삭제하면 된다.
- 글을 쓴 Member가 탈퇴하면?
이대로 구현 시키려면, 연결된 모든 객체의 생명주기가 하나로 관리되어야 한다.
따라서 모든 연관 관계에 CASCADE
를 거는 것이 맞다고 생각한다.
하지만 내가 간과한 부분이 있었다.
바로 CASCADE
의 세부 옵션에 대해서 너무 무지했던 것이다.
CASCADE
의 세부 옵션을 간단히 설명하면 아래와 같다.
- ALL : 아래의 Option들을 모두 적용한다
- PERSIST : 부모 Entity를 영속화 할 때, 자식도 함께 영속화한다.
- REMOVE : 부모 Entity를 제거할 때, 자식도 함께 제거한다.
- MERGE : 부모 Entity를 병합할 때, 자식도 함께 병합한다.
- DETACH : 부모가 Detach되면, 연관된 자식도 함께 Detach된다.
- 즉, 더 이상 둘 사이의 변경은 서로 전파되지 않는다.
- REFRESH : 부모가 Refresh(새로고침) 될 때, 자식도 함께 된다.
이 중에서 PERSIST
옵션의 동작에 의해 문제가 생겼다.
JPA의 공식문서를 확인해보니 다음과 같은 말이 있었다.
위의 글에 보면 PERSIST
의 경우, 부모 X가 Y에게 flush 마다 persist 상태를 전이한다고 되어있다.
사실상 일대다 - 다대일 관계는 부모가 둘이라고 볼 수 있다.
교차 엔티티에 대해서 두 명의 부모가 존재하는 것이다.
따라서 내가 하나의 부모를 삭제하더라도, flush
가 발생할 때 다른 쪽 부모가 persist 상태를 자식에게 전이해버리기 때문에, 교차 엔티티는 REMOVED
되었다가 다시 PERSIST
상태로 돌아가게 되는 것이다.
즉, 자식은 결국 PERSIST
상태이기에 삭제되지 않는다.
하지만 한 쪽 부모는 REMOVED
상태이기 때문에 DELETE
쿼리가 날아가게 된다.
그럼 당연히 자식과의 연관 관계가 살아있기 때문에 참조 무결성 오류가 발생하게 된다.
아래와 같이 REMOVED
로 모두 설정을 해주었더니, 안전하게 삭제가 되었다.
CASCADE.ALL
를 사용할 때는 고민해보고 적재적소에 사용해야함을 배웠다.