Spring Boot 테스트에서 @Transactional
을 사용할 때 동시성 테스트에서 예상치 못한 문제가 발생할 수 있다. 이러한 문제는 트랜잭션 격리 수준과 롤백 동작으로 인해 발생하며, 동시성 테스트를 정확히 진행하기 위해선 @Transactional
의 동작을 이해하고 적절한 대안을 선택해야 한다.
쿠폰 발급 API의 동시성 문제를 확인하기 위해 다음과 같은 테스트 코드를 작성했다:
@SpringBootTest
@Transactional
public class CouponConcurrencyTest {
@Test
void issueCoupon_concurrencyTest() throws InterruptedException {
int threadCount = 10;
ExecutorService executorService = Executors.newFixedThreadPool(threadCount);
CountDownLatch latch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
executorService.submit(() -> {
try {
couponService.issueCoupon(coupon.getId(), new RequestDto(member.getId()), member);
} catch (Exception e) {
System.out.println("예외 발생: " + e.getMessage());
} finally {
latch.countDown();
}
});
}
latch.await();
}
}
테스트 실행 결과, 아래와 같은 예외가 발생하며 동작하지 않았다:
예외 발생: 해당 쿠폰을 찾을 수 없습니다.
트랜잭션 격리로 인한 데이터 접근 제한:
@Transactional
은 트랜잭션이 종료되기 전까지 데이터 변경 사항을 해당 트랜잭션 범위 내에서만 볼 수 있다.테스트의 롤백 동작:
@Transactional
을 붙이면 테스트 종료 후 데이터가 자동으로 롤백된다.@Transactional
제거 및 명시적 데이터 초기화@Transactional
을 제거하고, 테스트 종료 후 명시적으로 데이터를 초기화한다.@AfterEach
를 활용하여 데이터 정리를 진행한다:@AfterEach
void tearDown() {
issuanceRepository.deleteAll();
couponRepository.deleteAll();
memberRepository.deleteAll();
}
READ_UNCOMMITTED
로 설정하여 다른 스레드가 커밋되지 않은 데이터를 읽을 수 있도록 허용할 수 있다:@Transactional(isolation = Isolation.READ_UNCOMMITTED)
@Transactional
을 제거하고 @AfterEach
로 데이터 초기화를 추가한 후, 테스트가 정상적으로 동작하며 쿠폰 발급 및 동시성 문제가 확인되었다:
남은 쿠폰 수량: 10 // 동시성 제어 문제로 중복 쿠폰 발급이 발생.
발급된 쿠폰 수량: 10
@Transactional
없이 명시적으로 관리하는 것이 동시성 테스트에 유리하다.이번 경험을 통해 Spring의 @Transactional
과 트랜잭션 격리 수준이 테스트 동작에 어떻게 영향을 미치는지 깊이 이해할 수 있었다. 특히 동시성 테스트는 단순히 코드를 작성하는 것만이 아니라, 테스트 환경과 트랜잭션 동작 방식에 대한 명확한 이해가 필요함을 느꼈다.