@SoftDelete 사용 시 테스트코드 영향 없애기

eora21·2024년 5월 17일
0

기존 테스트 코드를 @Transactional에 의한 롤백이 아닌, deleteAllInBatch로 데이터 정리를 구성하셨다면 @SoftDelete를 적용하셨을 때 예외가 발생할 수 있습니다.

이유인 즉슨, 실제 데이터 삭제가 아니라 deleted 필드를 업데이트한 것이기 때문에 이후 테스트에서 유니크 필드의 데이터가 중복 될 수 있습니다. 또는 @SoftDelete가 적용된 튜플이 @SoftDelete를 사용하지 않은 테이블과 관계를 지녀 삭제가 불가능해질 수 있습니다.

이를 해결하기 위해서는 @Transactional을 사용해 테스트 데이터를 롤백하던가, 혹은 강제 데이터 삭제를 구성해야 할 것입니다.

개인적으로는 deleteAllInBatch 형식을 통해 실제 코드의 트랜잭션까지 검증하는 방식을 선호하기 때문에 후자의 방식으로 진행해 보겠습니다.

HardDelete 코드 작성하기

기존 코드

@AfterEach
void tearDown() {
    commentRepository.deleteAllInBatch();
    postRepository.deleteAllInBatch();
    accountRepository.deleteAllInBatch();
}

기존의 deleteAllInBatch는 앞서 말씀드렸듯 softDelete를 수행하기 때문에 db에 데이터를 남기게 되고, 멱등성을 지킬 수 없게 됩니다.

HardDelete 수행할 @TestComponent 만들기

@Transactional
@TestComponent
public class HardDeleteSupplier {

    private final EntityManager entityManager;

    public HardDeleteSupplier(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    public void hardDelete(Class<?>... classes) {
        Arrays.stream(classes)
                .map(aClass -> aClass.getAnnotation(Table.class))
                .forEach(this::allDelete);
    }

    private void allDelete(Table table) {
        String tableName = table.name();
        entityManager.createNativeQuery("DELETE FROM " + tableName)
                .executeUpdate();
    }
}

@Table 어노테이션을 확인하여 테이블명을 가져온 후, 해당 테이블에 삭제 쿼리를 수행합니다.
(만약 @Table 어노테이션을 사용하지 않는 Entity가 존재하는 경우, 변환을 @Entity로 수행하신 후에 해당 클래스명에서 테이블명을 추출하는 코드를 작성하시면 되겠습니다.)

HardDelete 적용하기

@AfterEach
void tearDown() {
    hardDeleteSupplier.hardDelete(Comment.class, Post.class, Account.class);
}

그 후 @AfterEach를 해당하는 방법처럼 작성해주시면 되겠습니다.

Propagation.MANDATORY 대응하기

만약 기존에 테스트 코드에서 @Transactional을 사용하셨다가 deleteAllInBatch 방식으로 변경하셨을 경우, Propagation.MANDATORY 설정이 되어 있는 클래스를 동작시키실 때 문제가 발생할 수 있습니다.
이러한 경우에는 트랜잭션을 부여해 줄 수 있는 테스트용 컴포넌트를 생성하면 됩니다.

트랜잭션 발행할 @TestComponent

@TestComponent
@Transactional
public class TransactionWrapper {
    public <T> T commit(Supplier<T> supplier) {
        return supplier.get();
    }
}

그 후 MANDATORY가 설정되어있는 메서드를 실행시키면 되겠습니다.

...
Comment comment = transactionWrapper.commit(() ->
        commentSupplier.supply(post.getId(), account.getId(), "comment"));
...
profile
나누며 타오르는 프로그래머, 타프입니다.

0개의 댓글

관련 채용 정보