💥 문제 상황
JPA를 사용하다가 xxxRepository.delete(Entity entity)
메서드를 통해 엔티티를 삭제하고 싶었는데 아무리 해도 delete 쿼리가 발생하지 않는 문제가 있었다.
엔티티 삭제 코드
@Transactional
public void rejectOrExitSchedule(Long memberId, Long scheduleId) {
Schedule schedule = findScheduleById(scheduleId);
validateMemberNotOwner(memberId, schedule);
ScheduleMember scheduleMember = findScheduleMemberByMemberIdAndScheduleId(memberId, scheduleId);
scheduleMemberRepository.delete(scheduleMember);
}
테스트 코드
테스트 실행 결과
엔티티 말고 id를 인자로 받는 메서드를 통해 제거를 시도해도 JPA 구현체 내부에서는 다시 delete()를 사용하기 떄문에 마찬가지 결과가 나왔다.
id를 인자로 받는 메서드를 통해 제거를 시도
결국 다음과 같이 JPQL로 직접 delete 쿼리를 날리는 메서드를 작성하고 호출하는 방법으로 임시로 해결하긴 했지만, 왜 이러한 현상이 발생하는지 이해가 되질 않아 원인을 찾아보았다.
레포지토리 JPQL 코드
delete 메서드 호출
...
scheduleMemberRepository.deleteByMemberIdAndScheduleId(memberId, scheduleId);
...
실행 결과
🩺 원인
여기저기 검색해보니 의외로 잘 알려진 문제여서 쉽게 해결할 수 있었다. 문제의 원인은 영속성 컨텍스트에 삭제하려는 엔티티와 연관관계를 갖는 다른 엔티티가 존재하기 때문이었다.
삭제하려고 했던 엔티티는 일정에 참여하는 회원 정보를 저장하는 ScheduleMember
엔티티이고, 이 엔티티는 일정 엔티티인 Schedule
과 양방향 연관관계를 갖는다.
엔티티 연관관계
따라서, 앞서 살펴본 문제의 코드에서 검증 로직 수행을 위해 Schedule(ScheduleMember를 fetch join 함)을 먼저 조회하게 되면 영속성 컨텍스트에 Schedule 엔티티가 올라가게 되고, 이후 ScheduleMember를 조회하면 이미 영속성 컨텍스트에 동일 id의 ScheduleMember가 존재하기 때문에 select 쿼리 없이 캐시에서 엔티티가 조회된다.
디버깅을 통해 참조값을 확인해보면 Schedule의 List<ScheduleMember>
에 포함된 ScheduleMember와 이후에 조회한 ScheduleMember가 동일한 인스턴스임을 확인할 수 있다.
🔨 해결 방안
따라서 이 문제를 해결하기 위해서는 delete() 메서드 호출 전에 다른 엔티티와의 연관관계를 끊어주면 된다. 이 경우 Schedule의 리스트에서 ScheduleMember를 제거해주면 정상적으로 delete 쿼리가 나가는 것을 확인할 수 있다.
Schedule
다음과 같이 Schedule에 scheduleMembers 리스트에서 주어진 ScheduleMember을 제거하는 메서드를 만들고,
public void deleteScheduleMember(ScheduleMember scheduleMember) {
scheduleMembers.remove(scheduleMember);
}
아래와 같이 delete 쿼리를 날리기 전에 호출해주면,
repository.delete()
메서드를 통해서도 다음과 같이 제대로 delete 쿼리가 나가는 것을 확인할 수 있다.
마지막으로, 이런식으로 Schedule에서 ScheduleMember를 제거하도록 구현할거면 orphanRemoval 기능을 사용해서 별도로 ScheduleMemberRepository의 delete 메서드
를 호출하지 않고 Schedule의 deleteScheduleMember()
만 호출해도 delete 쿼리가 나가도록 구성해도 괜찮을 것 같다는 생각이 들었다.