통합 테스트에서
@Transactional
사용시 불편했던 점이랑 이를 개선할 수 있는 방법에 대해 고민해보고 해결책을 정리해본 글이다.
@Transactional
어노테이션은 테스트에서 사용시 테스트가 끝나면 DB의 변경사항을 롤백해 독립적인 테스트를 할 수 있도록 도와준다.
이는 여러개의 테스트에서 DB와 관련된 작업을 수행할때 테스트를 독립적으로 수행할 수 있어 굉장히 편리하게 사용할 수 있는 기능이다.
하지만 이번 프로젝트를 진행하며 테스트에서 @Transactional
을 사용하는것이 오히려 문제가 되었던 상황이 있었고, 그 상황에 대한 설명과 이를 해결할 수 있는 방법에 대해 알아보았다.
동시성 테스트를 진행하기 위해 샘플 상품 데이터를 만들어 두고 재고를 변경하는 테스트 코드를 짰다.
@Test
@DisplayName("동시에 여러번의 재고 변화 요청이 있더라도, 모두 반영되어야 함. (동시성 문제)")
@Transactional
public void changeStock_sync() throws Exception {
// given
sellerUserId = userService.joinUser(UserDtoUtils.getJoinUserRequestDto());
categoryId = categoryService.createCategory(CategoryDtoUtils.getCreateCategoryRequest());
productId = productService.createProduct(ProductDtoUtils.getCreateProductRequest(categoryId), sellerUserId);
productDetailId = productService.createProductDetail(ProductDtoUtils.getCreateProductDetailRequest().get(0),
productId);
ExecutorService executors = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
executors.submit(() -> {
productService.updateProductDetailStock(productDetailId, 10);
});
}
executors.shutdown();
executors.awaitTermination(1, TimeUnit.MINUTES);
ProductDetail productDetail = productService.getProductDetail(productDetailId);
Inventory inventory = inventoryRepository.findById(productDetail.getInventory().getId()).get();
assertThat(inventory.getQuantity()).isEqualTo(100);
}
하지만 데이터 생성이 트랜잭션 내부에서만 이루어지고 커밋이 되지 않은 상태에서 별도의 스레드에서 다른 트랜잭션으로 재고를 업데이트 하는 요청이 나갔다.
그러다 보니 데이터 생성과 데이터 수정의 트랜잭션이 다르고, 데이터 생성 트랜잭션이 끝나기 전에 데이터 수정 트랜잭션이 시작하다 보니 수정할 데이터를 찾지 못하는 문제가 발생했다.
@Transactional
을 사용하지 않고 DB에 데이터를 직접 생성했다.
그리고 이 데이터는 DB에 남는 데이터 이므로, @AfterEach
를 사용해 테스트가 끝나면 데이터를 삭제해 주었다.
이런 동작이 가능한 이유는, 모든 테스트는 별도의 인스턴스에서 수행되지만 따로 설정을 하지 않는이상 병렬로 수행되지는 않기 때문에 가능했다.
처음에는 DB에 직접 데이터를 만들게 되면 다른 테스트에 간섭이 생기는것이 아닌가? 했지만 실제 테스트가 진행되는것을 보니 테스트는 병렬이 아닌 하나씩 수행되었고 하나의 테스트 인스턴스가 생성될때 샘플 데이터를 생성하고 테스트가 끝나면 샘플 데이터를 삭제해주니 다른 테스트에 간섭또한 생기지 않았다.
하지만 @Transactional
을 사용하지 않았더니 발생하는 문제 또한 있었다.
재고 수량을 더한 결과를 확인하기 위해 테스트 코드에서 상품 -> 재고 의 연관관계를 사용해 쿼리를 날리려 했지만, LazyInitializationException
으로 할 수 없었다.
FetchType.LAZY
로 설정된 연관 관계의 데이터가 필요할때, 트랜잭션으로 묶여있을때는 영속성 컨텍스트가 유지되어 레이지 로딩을 할 수 있었지만 트랜잭션으로 묶여있지 않을때는 영속성 컨텍스트가 유지되지 않기 때문에 레이지 로딩이 불가능한것이다.
ProductDetail productDetail = productService.getProductDetail(productDetailId);
assertThat(productDetail.getInventory().getQuantity()).isEqualTo(100);
productDetail의 inventory
는 Lazy 로딩으로 설정되어 있고, 현재 프록시로 가지고 있지만 영속성 컨텍스트가 없는 상태에서 이를 조회하려 했기에 문제가 발생한다.
LAZY
로딩으로 설정된 연관관계는 해당 엔티티의 프록시를 가지고 있다. 그리고 이 프록시에 다른 정보는 없어도 식별자 값은 가지고 있다.
이 식별자 정보를 사용해 레포지토리를 사용해 쿼리를 날릴 수 있다.
ProductDetail productDetail = productService.getProductDetail(productDetailId);
Inventory inventory = inventoryRepository.findById(productDetail.getInventory().getId()).get();
assertThat(inventory.getQuantity()).isEqualTo(100);
@Transactional
은 DB와 연동이 필요한 테스트에서 다른 테스트에 영향을 주지 않으며 테스트를 편하게 진행할 수 있기에 정말 편한 어노테이션이다.
하지만 때때로 DB 상태 변화가 트랜잭션안에 갇혀있는 것이 문제가 될때도 있다.
특히 여러개의 트랜잭션으로 테스트를 진행할때는 트랜잭션 격리성 때문에 DB의 상태 변화를 추적할 수 없는 문제가 생길 수 있다.
이럴때는 @Transactional
을 사용하지 않고 DB에 직접 데이터를 추가하고 삭제하는 방법을 생각해볼 수 있다.
테스트는 별도로 설정하지 않는이상 하나씩 수행되기 때문에 하나의 테스트가 끝난 이후 사용한 데이터를 모두 삭제해 주기만 하면 깔끔하게 다음 테스트를 격리된 상태로 수행할 수 있다.