안녕하세요, 개발자 여러분. 오늘은 JPA를 사용하면서 겪은 재미있는 문제 상황과 그 해결 과정을 공유하고자 합니다.
문제 상황
최근 프로젝트에서 JPA를 사용하여 엔티티의 soft delete 기능을 구현했습니다. @SQLDelete 어노테이션을 사용하여 삭제 쿼리를 커스터마이징하고, @SQLRestriction을 통해 삭제된 엔티티를 조회에서 제외하도록 설정했습니다.

그리고 다음과 같은 테스트 코드를 작성했습니다:
예상치 못한 결과
테스트를 실행했을 때, 예상과 다른 결과가 나왔습니다. isDeleted() 메서드가 false를 반환한 것입니다.

원인 분석
이 문제의 원인을 분석해보니, 다음과 같은 점들을 놓쳤다는 것을 깨달았습니다:
- 영속성 컨텍스트와 데이터베이스의 불일치: @SQLDelete는 데이터베이스 레벨에서 작동하지만, 영속성 컨텍스트의 엔티티 상태는 변경하지 않습니다.
- 캐시된 엔티티: 테스트에서 사용한 facility 객체는 여전히 영속성 컨텍스트에 캐시된 상태였고, 데이터베이스의 변경사항을 반영하지 않았습니다.
해결 방법
이 문제를 해결하기 위해 다음과 같은 접근 방식을 사용할 수 있습니다:
1. 영속성 컨텍스트 초기화
entityManager.flush();
entityManager.clear();
2. 네이티브 쿼리 사용
: @SQLRestriction을 우회하는 네이티브 쿼리를 사용하여 삭제된 엔티티를 조회합니다.
Facility deletedFacility = (Facility) entityManager.createNativeQuery(
"SELECT * FROM facility WHERE name = :name", Facility.class)
.setParameter("name", facility.getName())
.getSingleResult();
-> @SQLRestriction 로 인해, where절에 is_deleted = false 구문이 포함되는 문제점으로 인해, 네이티브 쿼리로 조회합니다.
최종 테스트 코드 :
@DisplayName("soft delete가 되는 것을 확인한다.")
@Test
void soft_delete() {
// given
Facility facility = FacilityFixture.createFacility();
facilityJpaRepository.save(facility);
// when
facilityJpaRepository.delete(facility);
entityManager.flush();
entityManager.clear();
// then
Facility deletedFacility = (Facility) entityManager.createNativeQuery(
"SELECT * FROM facility WHERE name = :name", Facility.class)
.setParameter("name", facility.getName())
.getSingleResult();
Assertions.assertThat(facilityJpaRepository.findAll()).isEmpty();
Assertions.assertThat(deletedFacility.isDeleted()).isTrue();
}
교훈
이번 경험을 통해 몇 가지 중요한 교훈을 얻었습니다:
- JPA의 영속성 컨텍스트와 실제 데이터베이스 상태의 차이를 항상 인지해야 합니다.
- @SQLDelete와 같은 데이터베이스 레벨의 조작은 영속성 컨텍스트에 즉시 반영되지 않습니다.
- 테스트 시 entityManager.flush()와 clear()를 적절히 사용하여 영속성 컨텍스트를 관리해야 합니다.
- 복잡한 쿼리나 특수한 상황에서는 네이티브 쿼리 사용을 고려해볼 수 있습니다.
JPA는 강력한 도구이지만, 때로는 이런 세부적인 동작 방식 때문에 예상치 못한 결과를 마주할 수 있습니다. 이러한 경험들을 통해 JPA의 동작 원리를 더 깊이 이해하고, 더 나은 코드를 작성할 수 있게 되었습니다.
'JPA' 카테고리의 다른 글
OSIV와 지연로딩(feat. SSE) (0) | 2025.02.22 |
---|