Test Fixture 클렌징

테스트 환경에서 구성한 Fixture 들을 어떻게 하면 효율적으로 삭제할 수 있을까?

deleteAll vs deleteAllInBatch

class Test {

    @AfterEach
    void tearDown() {
        repository.deleteAllInBatch();
        repository.deleteAll();
    }
    ...

테스트 코드를 작성하다보면 위 처럼 테스트 환경의 독립성을 위해 구성되었던 환경을 모두 재설정하는 작업을 하게된다.

이 과정에서 deleteAll() 을 쓸지, deleteAllInBatch() 를 쓸지 결정해야 하는데 그 차이점에 대한 이해하는 내용이다.

예시 Entity

public class OrderProduct extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    private Order order;

    @ManyToOne(fetch = FetchType.LAZY)
    private Product product;

    public OrderProduct(Order order, Product product) {
        this.order = order;
        this.product = product;
    }
}

앞으로 설명하게 될 메서드에서 해당 코드 구조를 기반으로 테스트 코드의 AfterEach 내 클렌징 코드를 예시로 든다.

deleteAllInBatch

deleteAllInBatch 가 실행하는 쿼리를 보면 실제 테이블에 대한 아무런 조건 없이 모든 데이터를 삭제하게 된다.

그렇기 때문에 테스트 환경에서 생성 되었던 모든 데이터를 보다 빠르게(조건 없이 한 번에 삭제하기 때문) 삭제가 가능한데, 단점이 있다면 JPA 특성상 다른 객체와 연관 관계를 맺고 있어 서로 외래키로 강하게 묶여있는 경우는 삭제가 불가능하다.

이 때는 외래 키를 참조하고 있는 객체 먼저 삭제 해야하는 "순서" 를 고려해서 삭제 순서를 결정해야한다.

class OrderServiceTest {

    @Autowired
    private ProductRepository productRepository;

    @Autowired
    private OrderRepository orderRepository;

    @Autowired
    private OrderProductRepository orderProductRepository;

    @Autowired
    private StockRepository stockRepository;

    @Autowired
    private OrderService orderService;

    @AfterEach
    void tearDown() {
        orderProductRepository.deleteAllInBatch();
        orderRepository.deleteAllInBatch();
        productRepository.deleteAllInBatch();
        stockRepository.deleteAllInBatch();
    }

<예시 Entity 참고 >

OrderProduct 는 Order 와 Product 의 다대다 관계를 풀어주는 N:1 매핑 테이블이다.

만약 OrderServiceTest에서 tearDown 순서를 Order 먼저 제거하거나 Product 먼저 제거하는 경우 JPA Foreign Key 제약 사항으로 인한 에러를 발생시킨다.

이렇게 연관 관계를 조금 더 수월하게 삭제할 수 있는 메서드가 deleteAll() 이다.

deleteAll()

deleteAll() 의 쿼리를 살펴보면 Product 테이블에 존재하는 모든 데이터를 지우는 것은 동일 하지만, deleteAllInBatch() 의 delete from product 가 아닌 개별로 하나씩 접근해서 지우는 것을 확인할 수 있다.

또한 삭제하기 전 모든 데이터를 조건 없이 조회하는 것을 볼 수 있다.

이 때 deleteAllInBatch 가 갖고 있던 문제점 중 "연관 관계를 맺고있는 객체를 삭제할 수 없기 때문에 순서를 고려하여 삭제해야 한다." 라는 내용을 나름 해결할 수 있다.

<예시 Entity 참고 >

삭제하고자 하는 테이블에 모든 데이터를 조회할 때 관계를 맺고 있던 테이블도 같이 조회하여 삭제한다.

다만, 양방향인 경우 Order 를 먼저 삭제하든 Product 를 먼저 삭제하든 상관 없지만 Entity 관계를 보면 단방향으로 Product는 Order를 모르기 때문에 이 때 Product를 먼저 삭제하면 deleteAllInBatch 와 마찬가지로 외래 키 제약 에러가 발생한다.

즉, 객체가 다른 객체를 참조하고 있는 경우 또한 같이 조회 하여 삭제하기 때문에 관계 매핑을 순서에 비교적 자유롭게(아예 상관 없지는 않으니) 모두 지우고 싶다면 deleteAll 이 해결책이 될 수있다.

두 메서드의 차이는 어떻게 구현되어있을까?

	@Override
	@Transactional
	public void deleteAll() {

		for (T element : findAll()) {
			delete(element);
		}
	}

	@Override
	@Transactional
	public void deleteAllInBatch() {

		Query query = entityManager.createQuery(getDeleteAllQueryString());

		applyQueryHints(query);

		query.executeUpdate();
	}

실제 SimpleJpaRepository 의 deleteAll 과 deleteAllInBatch 내부 구현부분이다.

이 부분만 보더라도 둘의 차이를 명확히 알 수 있다.

서비스 계층에서 테스트 하는 영역의 독립성 보장하기섹션에서 다루는 SpringBootTest 와 Transactional 내용 중 tearDown 메서드를 권장하고, 트랜잭셔널은 가급적 자제하자 라고 말을 했지만 실제 트랜잭셔널 어노테이션이 테스트 환경에 대한 독립성을 보장하는데 굉장히 큰 편리함을 준다.

섹션에서는 트랜잭셔널을 사용했을 때 발생할 수 있는 사이드 이펙트를 정확히 인지하지 않은 채 사용하는 것을 지양하고 있으니 충분히 인지된 상태에서는 편리하게 롤백을 지원 받는 트랜잭셔널 어노테이션을 충분히 잘 활용하며 혼합적으로 사용하는 것을 추천한다.

그 외에도 스프링배치 처럼 트랜잭션에 대한 경계가 무수히 많아 트랜잭셔널을 적용하기 어려운 경우 위 처럼 순수하게 deleteAll 또는 deleteAllInBatch 을 고려해서 사용하면 된다.

절대적으로 모든것을 파훼할 방법이란 것은 없으니 상황에 맞게 트랜잭셔널을 사용하든, 티어다운 메서드를 작성하든 독립성을 위한 클렌징 작업을 한다는 것은 다름 없다.

Last updated