Soft Delete는 데이터베이스에서 레코드를 물리적으로 삭제하지 않고 상태 플래그를 변경해 비활성화 처리하는 방식입니다. 이를 구현한 후 예상대로 동작하는지 철저히 검증하는 테스트 전략이 중요합니다. 이번 글에서는 Soft Delete와 Delete의 내부 동작 방식, 그리고 Soft Delete 구현 시 테스트 코드 작성 방법에 대한 구체적인 전략을 다룹니다.
1. @SQLDelete와 @Where의 내부 동작 방식
1. @SQLDelete
@SQLDelete는 엔티티가 삭제될 때 Hibernate가 생성하는 기본 DELETE SQL 대신 사용자 정의 SQL을 실행하도록 설정하는 어노테이션입니다. 내부적으로 아래의 단계를 거칩니다.
1단계: Hibernate Entity Persister를 설정
Hibernate는 각 엔티티에 대해 EntityPersister를 생성합니다. 이는 엔티티에 대한 SQL 작업(삽입, 업데이트, 삭제 등)을 처리하는 핵심 컴포넌트입니다. @SQLDelete가 선선언된 경우, EntityPersister는 기본 DELETE 쿼리 대신 사용자 SQL 쿼리를 참조합니다. Hibernate는 EntityPersister.delete() 메서드를 호출할 때, 커스텀 SQL이 실행됩니다.
2단계: SQL 실행
@SQLDelete에 정정의된 SQL은 데이터베이스에 전달되어 실행됩니다. 보통 delete=true와 같이 상태 플래그를 업데이트 하는 SQL로 작성됩니다.
2. 엔티티의 영속성 컨텍스트와 Soft Delete
Hibernate의 영속성 컨텍스트는 엔티티의 상태를 관리합니다. 이때 Soft Delete를 구현할 때 고려해야합니다.
- 삭제된 엔티티도 영속성 컨텍스트에 남아 있을 수 있습니다.
- 영속성 컨텍스트에서 삭제된 엔티티를 처리하지 않으면 잘못된 데이터가 캐시에 남아 있을 수 있습니다.
해결책: EntityManager.clear() 또는 flush()를 적절히 사용하여 영속성 컨텍스트를 초기화하거나 동기화합니다.
3. Soft Delete 테스트 코드 작성 전략
Soft Delete는 물리적 삭제가 아닌 논리적 삭제가 되어야 하기에 실제 Database에서는 해당 값이 존재하여야 합니다. 하지만, @Where 조건을 활용해여 deletedAt=null인 경우에만 조회하도록 설계하였기 때문에 실제로 논리적 삭제가 잘 구현되었는지에 관해서는 알 수 없었습니다.
방법 1) @Rollback(value = false) 를 활용하여 로컬 Database에 직접 확인
장점: 간단히 로컬 DB에서 결과를 확인 가능하다.
단점: 매번 직접 확인해야 하므로 자동화되지 않는다.
제가 참고한 글에서는 @SpringBootTest를 통해서 통합 테스트를 진행시킨 후 @Transactional annotation을 활용해 테스트 할 수 있음 을 알 수 알 수 있었습니다.
@Test
@Rollback(value = false)
public void testSoftDelete() {
// given
Long id = createTestEntity();
// when
repository.deleteById(id);
// then
Optional<Entity> deletedEntity = repository.findById(id);
assertTrue(deletedEntity.isPresent());
assertNotNull(deletedEntity.get().getDeletedAt());
}
하지만, 매번 Soft delete가 제대로 동작하는지 확인하기 위해서 직접 매번 database에서 확인하는 방법은 괜찮아보이진 않았습니다.
방법 2) Where 조건절을 무시하는 쿼리를 작성
장점: 삭제된 데이터도 조회 가능해 논리적 삭제 확인에 적합하다.
단점: Native Query 사용 시 유지보수 복잡성이 증가한다.
그 다음 방법은, Where 조건 절을 무시하는 쿼리를 작성하는 것입니다. 사실 테스트코드를 작성할 때 외에도 탈퇴 혹은 삭제 처리 된 record를 가져와야 하는 상황에서 @Where 조건으로 원하는 데이터를 가져오지 못하는 경우가 있어 불편함을 느낀적이 있었습니다.
따라서 JPA의 Where 조건절을 무시하고자 Native Query를 사용하는 방법이 있습니다. 해당 코드는 다음과 같이 작성할 수 있습니다.
@Repository
public interface MemberCustomerRepository extends JpaRepository<MemberCustomer, Long> {
Optional<MemberCustomer> findByMemberIdAndCustomerIdAndDeletedAtIsNull(Long memberId, Long customerId);
Optional<MemberCustomer> findByMemberIdAndDeletedAtIsNull(Long memberId);
@Query(value = "SELECT * FROM MemberCustomer", nativeQuery = true)
Optional<MemberCustomer> findByAllMemberWithDeletedAt();
@Query(value = "SELECT * FROM MemberCustomer WHERE member_id = ?1", nativeQuery = true)
Optional<MemberCustomer> findByMemberIdWithDeletedAt(Long memberId);
}
위와 같이 Native Query를 활용한다면 deletedAt가 null이 아닌 값도 가져올 수 있습니다. 이를 활용하여 테스트코드에 적용시키면 실제로 논리적 삭제가 정상적으로 동작했는지 확인할 수 있습니다.
참고 문헌
'Backend' 카테고리의 다른 글
MySQL vs PostgreSQL데이터베이스의 성능 및 확장성 비교 (4) | 2024.10.31 |
---|---|
비동기 처리를 지원하는 모델(스레드 기반/이벤트 루프 기반) (0) | 2024.06.23 |
Presigned Url 으로 S3에 이미지 업로드하기 (Kotlin) (0) | 2024.04.19 |
Kotlin+Spring Kinesis 비동기 처리 EFO(Coroutine) (0) | 2024.03.20 |
스트리밍 데이터(Data Stream) (0) | 2024.03.18 |