인프런 영문 브랜드 로고
인프런 영문 브랜드 로고

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

조희제님의 프로필 이미지

작성한 질문수

Practical Testing: 실용적인 테스트 가이드

동시성 테스트와 데이터 초기화

작성

·

115

·

수정됨

0

안녕하세요 선생님 

배운 테스트 강의를 통해 사이드 프로젝트를 하며 성장 중입니다.

동시성 테스트를 하던 중 문제가 발생 하였고 해결은 하였으나 다른 방법이 있는지 조언을 구하고자

질문을 남기게 되었습니다.

 

먼저 엔티티에 대한 설명을 드리겠습니다.

모임과 모임 참여 테이블이 1 : N

모임 참여 회원 테이블이 N : 1 입니다.

모임은 최대 인원인 capacity를 가지고 있습니다.

여러 회원이 모임에 동시 참여 했을때 인원수를 제한 되는지 보기 위한 테스트 입니다.

동시성 문제를 막기 위해 비관적락을 모임을 조회 할때 사용 하였습니다.

 

DB는 MYSQL을 사용 하였습니다.

트랜잭션의 격리 수준은 기본인 REPEATABLE READ 을 사용 하였습니다.

테스트 트랜잭션에서 모임을 저장 하고 비동기 작업의 다른 스레드에서 모임 조회를 시도 할 경우

테스트의 모임 저장은 커밋되지 않은 트랜잭션으로 조회가 불가능한 문제가 있었습니다.

문제 해결 방안으로는

  1. 테스트 트랜잭션 어노테이션 제거

  2. 비동기 작업 스레드를 통한 트랜잭션 커밋으로 이후 트랜잭션이 모임을 조회 하는 방법

테스트의 일관성을 위해 2번 방법을 선택 하였습니다.

모임과 회원을 저장하는 부분을 비동기 작업 스레드를 통해 커밋 하고

이후 스레드의 트랜잭션으로 조회 하여 데이터를 읽을수 있도록 하였습니다.

 

테스트는 정상적으로 통과 되었지만 문제가 발생 했습니다.

스레드 작업의 트랜잭션 커밋으로 다른 테스트에 영향이 가는 문제 입니다.

 

원인은 테스트 트랜잭션이 전파 되지 않음에 따라 스레드 작업이 롤백 되지 않았습니다.

@Test
void not_executors_Tx() {
 log.info("외부 트랜잭션 시작");
 TransactionStatus outer = txManager.getTransaction
 (new DefaultTransactionAttribute());

 log.info("outer.isNewTransaction={}", outer.isNewTransaction());
 log.info("내부 트랜잭션 시작");
 TransactionStatus inner = txManager.getTransaction
 (new DefaultTransactionAttribute());
 log.info("inner.isNewTransaction={}", inner.isNewTransaction());

}

결과 : outer.isNewTransaction = true / inner.isNewTransaction = true

@Test
void executors_Tx() {
        //given
  log.info("외부 트랜 잭션 시작");
  ExecutorService executorService = Executors.newFixedThreadPool(5);
  TransactionStatus outer = txManager.getTransaction
  (new DefaultTransactionAttribute());
  log.info("outer.isNewTransaction() = {}", outer.isNewTransaction());

  log.info("내부 트랜 잭션 시작");
  executorService.submit(() -> {
      TransactionStatus inner = txManager.getTransaction
      (new DefaultTransactionAttribute());
      log.info("inner.isNewTransaction() = {}", inner.isNewTransaction());
    });
        executorService.shutdown();
}

결과 : outer.isNewTransaction = true / inner.isNewTransaction = false

 

커밋한 스레드의 결과로 인해 전체 테스트에 영향이 가게 되었습니다.

밑의 페이징 정보 조회 테스트를 실행 하기 전에 모임 전체 조회를 해보니 테스터 라는

스레드 작업에서 저장한 모임이 조회 되었습니다.

 

어떻게 하면 커밋된 데이터들이 다른 테스트에 영향을 주지 않을까?를 고민 하였고

생각한 해결 방안은 @AfterEach를 사용 하자 였습니다.

하지만 해결 되지 않았습니다.

 

다른 테스트 에선 여전히 커밋된 테스터 모임이 발견 되었습니다.

이 부분은 왜 delete가 되지 않았는지 모르겠습니다.

추측 하기로는 REPEATABLE READ 격리 수준에서 자신이 트랜잭션을 시작 하였을때 데이터만

조회 하고 삭제 할수 있기 때문에 테스트 트랜잭션 에서는 스레드가 추가한

데이터를 조회, 삭제가 불가능 해서 발생한 문제 이지 않을까 하는 추측 입니다.

 

그레서 모든 테스트에 @BeforeEach를 사용 하여 테스트 시작전에

데이터를 모두 지우고 테스트 하니 통과 하였습니다.

 

선생님께 드리고 싶은 질문은 이렇게 해결 한것이 최선인지 궁금합니다.

멀티 스레드는 테스트 할때 어떻게 동작 할지 모르기 때문에

트랜잭션 어노테이션을 제거 하는 방법이 더 나을까요?

 


@AfterEach
void cleanUp() {
   userEventRepository.deleteAllInBatch();
   bookmarkRepository.deleteAllInBatch();
   eventRepository.deleteAllInBatch();
   userRepository.deleteAllInBatch();
}


@DisplayName("페이징 정보를 받아 모임을 조회 합니다.")
@Test
void getPagingEvents() {
        //given
        for (int i = 0; i < 10; i++) {
            Event event = createEvent("테스터" + i, "자전거 모임");
            eventRepository.save(event);
        }

        Pageable pageable = PageRequest.of(1, 3);

        //when
        Slice<EventPreviewResponseDto> slice = eventService.getPagingEvents(pageable);

        //then
        assertThat(slice.getContent())
                .extracting("author")
                .containsExactlyInAnyOrder("테스터3", "테스터4", "테스터5");
}
@DisplayName("5명의 회원이 동시에 최대 인원이 3명인 모임에 참가 할때 3명만 참여 할 수 있다.")
@Test
void joinEventWhenParticipateAtTheSameTimeWithConcurrency() throws Exception {
    //given
    int taskCount = 5;
    ExecutorService executorService = Executors.newFixedThreadPool(5);
    CountDownLatch countDownLatch = new CountDownLatch(taskCount);

    Event findEvent = executorService.submit(() ->
            eventRepository.save(createEvent("테스터", "테스트 모임", 3))).get();

    List<User> users = executorService.submit(() -> Stream
            .generate(() -> {
                User user = createUser("테스터", "testEmail");
                userRepository.save(user);
                return user;
            })
            .limit(taskCount)
            .toList()).get();

    //when
    AtomicInteger exceptionCount = new AtomicInteger(0);

    for (User user : users) {
        executorService.submit(() -> {
            try {
                eventService.joinEvent(findEvent.getId(), user.getId());
                eventRepository.flush();  // 엔티티 상태를 DB에 강제로 반영
            } catch (BusinessException ex) {
                exceptionCount.incrementAndGet();
            } finally {
                countDownLatch.countDown();  // 카운트다운
            }
        });
    }
    countDownLatch.await();

    Long participateCount = executorService.submit(
            () -> userEventRepository.countParticipantByEventId(findEvent.getId())).get();

    executorService.shutdown();

    //then
    assertThat(participateCount).isEqualTo(3);
    assertThat(exceptionCount.get()).isEqualTo(2);
}

답변 2

1

박우빈님의 프로필 이미지
박우빈
지식공유자

안녕하세요, 조희제 님!

말씀 주신 상황을 제가 글로만 보다보니 100% 이해하기는 어렵지만, 이미 문제와 원인 파악을 잘 해주신 것 같아요.
BeforeEach로 접근하신 방법도 나쁘지는 않습니다만, 확인하신대로 멀티 스레드 테스팅 환경에서는 해당 테스트에 영향을 줄 수 있는 테스트 어노테이션을 제거하고 접근하는 것이 더 좋아 보입니다.

감사합니다 🙂

0

인프런 AI 인턴님의 프로필 이미지

안녕하세요, 인프런 AI 인턴입니다. 조희제님, 동시성 테스트와 관련된 문제에 대해 고민을 공유해 주셨네요. 모임과 모임 참여에 대한 동시성 문제는 정확한 데이터 관리를 위해 중요한 이슈입니다. 관련하여 제가 본문에서 공유하신 내용과 유사한 질문에 대한 답변을 찾아보았습니다.

먼저, 비동기 작업에서 커밋되지 않은 데이터가 다른 테스트에 영향을 미치는 문제에 대한 질문입니다. SQL과 트랜잭션 관리와 관련한 문제에 비슷한 해결 방안을 찾았습니다. 테스트 동시성 관련 질문드립니다에서 소개된 예제들에서는 비동기 작업이 완료되는 때까지 기다리는 방법으로 CountDownLatch 또는 Future.get()을 활용해야 할 뿐만 아니라, 트랜잭션 경계에서의 적절한 롤백과 커밋 관리를 제안하고 있습니다. 이와 같은 기법을 사용해 비동기 작업이 완료된 후 적절하게 데이터가 정리되도록 할 수 있습니다.

또한, 모든 테스트에 @BeforeEach를 사용하는 해결 방법은 테스트 케이스 간의 독립성을 보장하는 좋은 접근 방법입니다. 하지만 전체 DB를 삭제하고 다시 데이터를 삽입하는 방법은 테스트의 실행 시간을 증가시킬 수 있으니, 필요한 경우 최소한의 데이터를 정확하게 제거하거나 초기화하는 방법을 고려하는 것이 좋습니다.

비동기 작업 스레드에서 트랜잭션이 테스트 트랜잭션의 커밋되지 않은 데이터를 읽지 못하는 문제를 해결하기 위한 방법 중 하나로는, 테스트 내에서 명시적으로 데이터를 커밋하거나 플러시하여 비동기 스레드가 데이터를 정확하게 읽을 수 있도록 보장하는 것입니다. eventRepository.flush()와 같은 메서드를 적재적소에 배치함으로써, 필요한 데이터가 동기적으로 커밋되도록 할 수 있습니다.

저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드리고 있습니다.
현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏
추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.