[트러블슈팅] JPA 양방향 관계 설정 시 삭제가 되지 않는 문제

2025. 5. 29. 10:48·트러블슈팅

Tripin 프로젝트를 진행하던 와중 삭제가 되지 않는 문제가 발생했다.

 

문제 상황

TravelSchedule과 Expense는 양방향 OneToOne 관계이다.

아래는 간략한 엔티티 코드이다.

 

- TravelSchedule

@Getter
@Entity
public class TravelSchedule extends BaseEntity {

    @OneToOne(mappedBy = "travelSchedule", cascade = CascadeType.PERSIST, orphanRemoval = true)
    private Expense expense;
}

영속성 전이와 고아 객체를 설정해두었다.

TravelSchedule이 하나의 expense를 가지고 있다.

 

- Expense

@Getter
@Entity
public class Expense extends BaseEntity {

    @OneToOne
    @JoinColumn(name = "travel_schedule_id")
    private TravelSchedule travelSchedule;
}

Expense는 schedule을 참조하고 있다.

 

expense가 schedule 없이 독립적으로 존재하지 못하고

schedule이 삭제될 때 expense도 삭제되어야하기 때문에

cascade = CascadeType.PERSIST, orphanRemoval = true를 설정하여 관리하였다.

 

테스트 결과

-  expense 삭제 로직

    @Transactional
    public void deleteExpense(Long expenseId) {
        Expense expense = expenseRepository.findById(expenseId)
            .orElseThrow(() -> new NotFoundException(Message.EXPENSE_NOT_FOUND));

        expenseRepository.delete(expense);
    }

 

- 테스트 코드

@DisplayName("지출을 삭제합니다.")
@Test
void deleteExpense() {
    // given
    Expense expense = Expense.builder()
        .expectPrice(10000)
        .payedPrice(8000)
        .travelSchedule(schedule)
        .build();
    expenseRepository.save(expense);
    Long expenseId = expense.getExpenseId();

    // when
    expenseService.deleteExpense(expenseId);
    em.flush();
    em.clear();

    // then
    assertThat(expenseRepository.findById(expenseId)).isEmpty();
}

expense를 삭제하는 테스트를 작성하였다.

 

로그를 보니 delete 쿼리가 아예 나가지 않아 삭제가 되지 않았고 그렇기에 테스트도 실패하였다.

 

해결 방법

왜 delete 쿼리가 나가지 않는지 이해가 되지 않아

JPA 연관관계 매핑과 영속성 컨텍스트를 다시 공부하여 문제를 해결할 수 있었다.

 

@Transactional
public void deleteExpense(Long expenseId) {
    Expense expense = expenseRepository.findById(expenseId)
        .orElseThrow(() -> new NotFoundException(Message.EXPENSE_NOT_FOUND));

    TravelSchedule schedule = expense.getTravelSchedule();
    if (schedule != null) {
        schedule.deleteExpense();
    }

    expenseRepository.delete(expense);
}

 

em.remove()와 schedule -> expense의 cascade 연관관계가 서로 충돌해서

expense를 삭제해도 schedule -> expense 연관관계가 cascade로 남아있기 때문에 삭제가 되지 않았다.

영속성 컨텍스트 입장에서는 부모(schedule)가 여전히 자식을 참조하고 있어

delete와 persist(cascade)의 상태가 모순되어 flush 시점에 delete를 누락시킨 것이다.

 

그렇기 때문에 부모-자식 관계를 먼저 해제한 뒤 자식을 삭제해야한다.

schedule -> expense의 연관관계를 끊어주었더니 delete 쿼리가 나가는 것을 확인할 수 있었다.

연관관계가 없으니 cascade 효과도 사라지게 되어 정상적으로 작동하게 된 것이다.

 

 

cascade 옵션을 쓸 때 부모 엔티티를 삭제 or 수정할 때는 문제가 되지 않지만

자식 엔티티를 직접 삭제하고 싶을 때에는 객체 간의 연관관계를 반드시 먼저 끊고 삭제해야함을 알 수 있었다.

 

정리

1. 부모 컬렉션/참조에서 먼저 제거

2. 그 다음 delete() 호출

=> delete 쿼리 정상적으로 발생

 

참고 - 김영한님 답변 링크

https://www.inflearn.com/community/questions/56718/cascade-cascadetype-all%EC%A7%88%EB%AC%B8

저작자표시 비영리 변경금지 (새창열림)

'트러블슈팅' 카테고리의 다른 글

[트러블 슈팅] 페이징 쿼리 최적화 - offset, no offset, covering index  (1) 2025.09.21
[트러블슈팅] 인덱스로 쿼리 최적화하기  (0) 2025.09.17
[트러블슈팅] 동시성 문제 해결하기 - 낙관적 락, 비관적 락  (0) 2025.09.06
[트러블슈팅] JPA N+1 문제와 해결법  (0) 2025.06.27
'트러블슈팅' 카테고리의 다른 글
  • [트러블 슈팅] 페이징 쿼리 최적화 - offset, no offset, covering index
  • [트러블슈팅] 인덱스로 쿼리 최적화하기
  • [트러블슈팅] 동시성 문제 해결하기 - 낙관적 락, 비관적 락
  • [트러블슈팅] JPA N+1 문제와 해결법
도탱
도탱
ღ 성장하는 백엔드 개발자 ღ
  • 도탱
    도탱이의 코딩흔적
    도탱
  • 전체
    오늘
    어제
    • 분류 전체보기 (43)
      • Programing Language (4)
        • Python (3)
        • JAVA (1)
      • Web (28)
        • Spring (24)
        • 배포 (4)
      • 알고리즘 (2)
      • 자격증 (2)
      • 회고 (2)
      • 트러블슈팅 (5)
  • 인기 글

  • hELLO· Designed By정상우.v4.10.3
도탱
[트러블슈팅] JPA 양방향 관계 설정 시 삭제가 되지 않는 문제
상단으로

티스토리툴바