인프런 커뮤니티 질문&답변

chobo님의 프로필 이미지

작성한 질문수

토비의 스프링 6 - 이해와 원리

멀티 스레드를 사용하는 테스트에서 트랜잭션 사용에 대해서..

작성

·

231

·

수정됨

0

멀티 스레드를 사용하는 테스트에서는 @Transactional 을 어떻게 사용해야 할까요..?

 

동시성 이슈를 확인하는 테스트에서는 주로 ExecutorService 를 통해 스레드 풀을 생성해서 동시에 서비스 로직을 호출하는 식으로 테스트를 진행하면서 여러 문제를 겪었는데 어떻게 해결하면 좋을지 고민을 하다가 토비님에게 질문드려봅니다 ㅠㅠ

  1. 다른 스레드에서 save() 한 오브젝트를 조회할 수 없는 문제.

    1. 테스트 코드에 @Transactional 를 사용하게 되면 테스트 코드가 하나의 트랜잭션으로 묶이게 되는데, 다른 스레드에서 서비스를 호출 하기 전에 테스트를 위한 데이터를 save() 하고 다른 스레드가 서비스에서 save 한 데이터를 가져오려고 할 때 아직 DB에 commit 되지 않은 상태여서 가져오지 못하는 이슈가 있더라구요. -> db isolation level이 read uncommited 보다 높기 때문에 발생하는 것 같음.

          @Test
          @DisplayName("밖에서 저장한 데이터는 스레드 내부에서 가져올 수 없음")
          @Transactional
          void outsideTransaction() throws InterruptedException {
              Member member = new Member(1L, "테스트유저");
              memberRepository.saveAndFlush(member);
              entityManager.clear();
      
              TimeUnit.SECONDS.sleep(2);  // 일정 시간 대기.
      
              try (ExecutorService executorService = Executors.newFixedThreadPool(32)) {
                  executorService.submit(() -> {
                      long count = memberRepository.count();
                      Optional<Member> optionalMember = memberRepository.findById(1L);
                      System.out.println("member count: " + count);
                      System.out.println("optionalMember.isEmpty: " + optionalMember.isEmpty());
                  });
              }
              System.out.println("=================================");
              long count = memberRepository.count();
              Optional<Member> optionalMember = memberRepository.findById(1L);
              System.out.println("member count: " + count);
              System.out.println("optionalMember.isEmpty: " + optionalMember.isEmpty());
          }
      member count: 0
      optionalMember.isEmpty: true
      =================================
      member count: 1
      optionalMember.isEmpty: false
  2. 테스트 코드에 @Transactional 을 선언해도 테스트 코드 내부에서 생성한 스레드에서는 트랜잭션이 존재하지 않는 문제.

        @Test
        @DisplayName("내부 스레드는 트랜잭션이 존재하지 않음.")
        @Transactional
        void activeTransaction() {
            System.out.println("쓰레드 밖에서 트랜잭션 상태: " + TransactionSynchronizationManager.isActualTransactionActive());
            try (ExecutorService executorService = Executors.newFixedThreadPool(32)) {
                executorService.submit(() -> {
                    System.out.println("쓰레드 안에서 트랜잭션 상태: " + TransactionSynchronizationManager.isActualTransactionActive());
                });
            }
        }

     위와 같은 테스트를 실행 했을 때, 출력되는 결과는 다음과 같았습니다.

    쓰레드 밖에서 트랜잭션 상태: true
    쓰레드 안에서 트랜잭션 상태: false

멀티 스레드를 사용하는 테스트에서 @Transactional 를 사용 했을 때 이런 문제점이 있었는데요.. 여러가지 시도해보다가 해결을 하지 못해서 결국 @Transactional 을 사용하지 않고.. 테스트가 끝날 때 마다 @AfterEach 통해서 DB를 직접 제거하는 방식으로 하고 있습니다... 이렇게 테스트는 @Transactional 을 사용하지 않고 서비스 로직에는 @Transactional 사용하고 있다보니, 실제 애플리케이션을 띄워서 api 부하 테스트를 해보면 원하는 결과가 나오지 않았습니다. 테스트 코드가 테스트 코드의 역할을 수행하고 있지 않네요

이런 경우에는 어떻게 해결하는 것이 좋을까요?

 

감사합니다.

답변 4

1

토비님의 프로필 이미지
토비
지식공유자

API 레벨에서 DB 데이터의 동시성 문제를 다루고 싶다면 이건 DB 트랜잭션과 락을 사용하는 방식으로 해결해야 합니다. 테스트를 만들 더라도 모든 작업이 완료된 뒤에(즉 여러개의 트랜잭션 처리가 다 정상적으로 실행완료된 뒤에) 최종 값을 검증해야 하기 때문에 @Transactional 테스트는 어떤 식으로든 사용할 수 없습니다. @Transactional 테스트는 단일 트랜잭션을 롤백하는 것인데, API 수준의 동시성이라면 동시에 API를 실행하는 순간 이미 트랜잭션이 API마다 하나씩 따로 생기는 시나리오니 이건 단일 트랜잭션 롤백이라는 방식을 사용하는 테스트 기법의 대상에서 완전히 벗아난 것입니다.

ExecutorService를 사용할 때는 트랜잭션 시작 이전, 즉 Controller에 대해서 테스트 하시는 것이 좋습니다. 개별적으로 새로운 트랜잭션이 만들어질 것이고요. 혹은 mockMvc를 이용한 MVC테스트를 이용하셔도 됩니다.

지금 준비하는 강의에서 데이터 처리 동시성 관련 문제를 한번쯤 다룰텐데 그때 구체적인 예를 소개해드리겠습니다. 그 전에라도 검색해보시면 동시성 문제를 다루는 좋은 블로그 글을 찾으실 수도 있을 겁니다.

 

1

토비님의 프로필 이미지
토비
지식공유자

그런데 예로 드신 코드는 문제가 있습니다. ExecutorService에서 새로운 쓰레드를 만들고, 이 쓰레드에서 db 액세스를 하면, repository에서 새로운 트랜잭션이 만들어지는데, 이러면 고립도 설정에 따라서 같은 데이터를 두 개의 쓰레드에서 액세스할 때 어떤 결과가 나올지 모릅니다. 트랜잭션이 완료되지 않(을 수도 있)는 상태에서 두 개가 어떤 순서대로 진행될지 모르기 때문이죠.

현실에서는 절대 이런 스타일로 코드를 작성하시면 안 됩니다. 해결할 수 있는 문제도 없고, 멀티 쓰레드에서 어떤 타이밍에 어떤 코드가 실행될지 예측도 불가능합니다. 멀티쓰레드에서 서로 동기화를 잘 하지 않으면 안 되는데, 굳이 이렇게 코드를 작성할 필요도 없는데 이렇게 만드는 건 불필요할 뿐입니다.

 

chobo님의 프로필 이미지
chobo
질문자

네 답변 감사합니다!

1

토비님의 프로필 이미지
토비
지식공유자

@Transactional 롤백 테스트를 사용하는 데 두 가지 제약사항이 있습니다. 이건 공식 문서에도 구체적으로 업급됩니다.

첫번째는 지금 처럼 멀티 쓰레드를 사용하는 것이고, 두 번째는 @Transactional의 propagation을 REQUIRES_NEW로 지정해서 별개의 트랜잭션을 만드는 것입니다. 이 두가지 케이스에선 테스트에서 @Transactional을 사용하시면 안 됩니다. @Transactional은 테스트에서 트랜잭션을 시작한 뒤에 ThreadLocal에 바인딩하고 해당 쓰레드와 해당 트랜잭션만 롤백시키기 때문입니다.

이런 특별한 케이스에선 트랜잭션을 다 커밋시키고 테스트에서 검증한 뒤에 해당 데이터를 삭제해주는 방법을 쓰셔야 합니다.

특히 @Async라든가 멀티 쓰레드를 만들어서 그 안에서 DB 조작이 일어나는 경우, 모든 쓰레드 작업이 완료된 것을 확인하고 테스트에서 검증을 해야 합니다. 그렇지 않으면 테스트 결과가 항상 동일하게 나오지 않을 수도 있습니다.

0

안녕하세요, 인프런 AI 인턴입니다.

멀티 스레드 환경에서 @Transactional을 사용하는 것에 대한 문제는 Spring 환경에서 종종 마주칠 수 있는 어려운 상황 중 하나입니다. chobo님의 질문에서 제기된 두 가지 문제에 대해 다루겠습니다.

  1. 다른 스레드에서 save()한 객체를 조회할 수 없는 문제

이 문제는 다수의 스레드가 동일한 트랜잭션 컨텍스트를 공유하지 않기 때문에 발생합니다. @Transactional 어노테이션이 붙은 테스트는 하나의 트랜잭션으로 묶이게 되며, 기본적으로 트랜잭션이 커밋되기 전까지 다른 스레드에서는 그 변경사항을 볼 수 없습니다. 테스트 환경에서는 통상 트랜잭션의 롤백을 통해 데이터 일관성을 유지합니다.

해결책 중 하나는 테스트에서 데이터를 사전에 준비하고 커밋하는 별도의 트랜잭션을 사용하는 것입니다. 즉, 테스트 준비 단계에서 필요한 데이터를 사전에 저장하고 커밋하여, 멀티 스레드 환경에서 실행되는 테스트에서도 이 데이터를 조회할 수 있도록 하는 것입니다. 테스트가 완료된 이후에는 @AfterEach 등을 이용하여 데이터를 정리할 수 있습니다.

  1. 테스트 코드에 @Transactional을 선언해도 테스트 코드 내부에서 생성한 스레드에서는 트랜잭션이 존재하지 않는 문제

테스트 메서드에 @Transactional을 사용하면 해당 테스트 메서드는 하나의 트랜잭션 내에서 실행되나, 신규로 생성한 스레드는 그 트랜잭션 컨텍스트를 상속받지 못합니다. 따라서 신규 스레드에서는 별개의 트랜잭션으로 분리되어 처리됩니다.

해결 방안으로는, 스레드 내부에서 별도의 트랜잭션 경계를 명시적으로 시작하는 것입니다. Spring의 TransactionTemplate 또는 프로그래밍적 트랜잭션 관리를 통해, 스레드 내에서 직접 트랜잭션을 시작하고 종료하는 방식을 사용할 수 있습니다. 하지만, 이렇게 할 경우 복잡도가 증가하므로, 테스트의 목적을 달성할 수 있는 다른 방법이 있는지 먼저 고려하는 것이 좋습니다.

이와 같은 문제는 멀티 스레드 환경에서의 트랜잭션 관리와 관련된 한계점으로 볼 수 있으며, 테스트 설계 단계에서 이러한 환경의 복잡성을 고려하여 접근하는 것이 필요합니다. 멀티 스레딩과 트랜잭션의 복합적인 사용은 특히 주의 깊게 설계할 필요가 있습니다.

chobo님의 프로필 이미지

작성한 질문수

질문하기