@Transactional과 동시성 테스트의 문제점 및 해결 방법

coldrice99·2024년 12월 13일
0
post-thumbnail

Spring Boot 테스트에서 @Transactional을 사용할 때 동시성 테스트에서 예상치 못한 문제가 발생할 수 있다. 이러한 문제는 트랜잭션 격리 수준과 롤백 동작으로 인해 발생하며, 동시성 테스트를 정확히 진행하기 위해선 @Transactional의 동작을 이해하고 적절한 대안을 선택해야 한다.


1️⃣ 문제 상황

쿠폰 발급 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();
    }
}

테스트 실행 결과, 아래와 같은 예외가 발생하며 동작하지 않았다:

예외 발생: 해당 쿠폰을 찾을 수 없습니다.

2️⃣ 원인 분석

  1. 트랜잭션 격리로 인한 데이터 접근 제한:

    • @Transactional은 트랜잭션이 종료되기 전까지 데이터 변경 사항을 해당 트랜잭션 범위 내에서만 볼 수 있다.
    • 동시성 테스트에서 다른 스레드가 데이터에 접근하려 하면, 커밋되지 않은 데이터에 접근할 수 없어 조회에 실패한다.
  2. 테스트의 롤백 동작:

    • 테스트 메서드에 @Transactional을 붙이면 테스트 종료 후 데이터가 자동으로 롤백된다.
    • 롤백 동작으로 인해 데이터가 제대로 반영되지 않아 다른 스레드가 동일한 데이터를 사용할 수 없다.

3️⃣ 해결 방안

1. @Transactional 제거 및 명시적 데이터 초기화

  • 테스트 메서드에서 @Transactional을 제거하고, 테스트 종료 후 명시적으로 데이터를 초기화한다.
  • @AfterEach를 활용하여 데이터 정리를 진행한다:
@AfterEach
void tearDown() {
    issuanceRepository.deleteAll();
    couponRepository.deleteAll();
    memberRepository.deleteAll();
}

2. 트랜잭션 격리 수준 조정 (비권장)

  • READ_UNCOMMITTED로 설정하여 다른 스레드가 커밋되지 않은 데이터를 읽을 수 있도록 허용할 수 있다:
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
  • 하지만 이는 Dirty Read(더티 읽기)와 같은 데이터 무결성 문제가 발생할 가능성이 있어 권장되지 않는다.

3. 동시성 테스트를 별도의 통합 테스트로 진행

  • 동시성 문제를 테스트할 때는 데이터베이스와 트랜잭션 관리 없이, 독립적으로 테스트를 진행한다.
  • 트랜잭션 롤백과 같은 문제를 회피하며, 동시성 시나리오를 단순화할 수 있다.

4️⃣ 적용 결과

@Transactional을 제거하고 @AfterEach로 데이터 초기화를 추가한 후, 테스트가 정상적으로 동작하며 쿠폰 발급 및 동시성 문제가 확인되었다:

남은 쿠폰 수량: 10 // 동시성 제어 문제로 중복 쿠폰 발급이 발생.
발급된 쿠폰 수량: 10

5️⃣ 배운 점

  • 트랜잭션 격리와 롤백은 테스트 환경에서 예상치 못한 문제를 일으킬 수 있다.
  • 동시성 테스트를 수행할 때는 트랜잭션 관리 방식을 조정하거나 제거하여 테스트 환경을 적합하게 설정해야 한다.
  • 테스트 데이터 관리는 @Transactional 없이 명시적으로 관리하는 것이 동시성 테스트에 유리하다.

💭 회고

이번 경험을 통해 Spring의 @Transactional과 트랜잭션 격리 수준이 테스트 동작에 어떻게 영향을 미치는지 깊이 이해할 수 있었다. 특히 동시성 테스트는 단순히 코드를 작성하는 것만이 아니라, 테스트 환경과 트랜잭션 동작 방식에 대한 명확한 이해가 필요함을 느꼈다.

profile
서두르지 않으나 쉬지 않고

0개의 댓글