락을 사용한 동시성 테스트하기

2024. 2. 27. 09:00· 개발/[우테코]
목차
  1. 0. 들어가기 전에
  2. 1. 어떻게 테스트 할까?
  3. 2. 잘 동작할까?
  4. 2.1 내부 트랜잭션 문제
  5. 2.2 given 절의 데이터 초기 세팅 문제
  6. ※ 참고

0. 들어가기 전에

앞선 글에서 TaskScheduler 사용한 기능을 만들었다. 그중 WAS가 많아지면 생길 Race Condition 때문에 비관적 락을 적용했다. 이 부분을 테스트하기 위해 고민한 글입니다.



1. 어떻게 테스트 할까?

동시 요청을 테스트하려면 어떻게 할 수 있을까? 이를 위한 클래스가 자바에 있다. 바로 ExecutorService다. ExecutorService는 쓰레드 풀을 만들어 비동기적으로 동작하는 것을 돕는 클래스이다. 자세한 방법은 코드로 알아보자.

@DisplayName("다수의 스케줄이 동시에 실행되도 ReviewCount는 1개만 증가한다.")  
@Test  
void finishReview_when_multiple_request() throws InterruptedException {  
    // given  
    final Runner runner = persistRunner(MemberFixture.createHyena());  
    final RunnerPost runnerPost = persistRunnerPost(runner);  
    final Supporter supporter = persistSupporter(MemberFixture.createEthan());  
    persistAssignSupporter(supporter, runnerPost);  
    deadlineOutboxCommandRepository.save(DeadlineOutboxFixture.deadlineOutbox(runnerPost.getId(), Instant.now()));  

    final ReviewCount originReviewCount = ReviewCountFixture.reviewCount(supporter.getReviewCount().getValue());  
    final int requests = 10;  
    final ExecutorService executorService = Executors.newFixedThreadPool(requests);  
    final CountDownLatch latch = new CountDownLatch(requests);  

    // when  
    for (int i = 0; i < requests; i++) {  
        executorService.submit(() ->  
            {  
                try {  
                    runnerPostDeadlineCheckScheduler.finishReview(runnerPost.getId());  
                } catch (final Exception e) {  
                } finally {  
                    latch.countDown();  
                }  
                return null;  
            }  
        );  
    }  

    latch.await();  
    executorService.shutdown();  

    // then  
    final RunnerPost actual = runnerPostQueryRepository.joinSupporterByRunnerPostId(runnerPost.getId()).get();  
    assertAll(  
            () -> assertThat(actual.getSupporter().getReviewCount()).isEqualTo(ReviewCountFixture.reviewCount(originReviewCount.getValue() + 1)),  
            () -> assertThat(actual.isReviewStatusDone()).isTrue(),  
            () -> assertThat(deadlineOutboxCommandRepository.findAll()).isEmpty()  
    );  
}
  • given 절에서 데이터를 세팅하고, ExecutorService를 통해 쓰레드를 10개를 생성한다. 그 후 비동기 상황에서 실행 개수를 확인하기 위해 CountDownLatch를 통해 10개의 요청을 셀 수 있게 세팅한다.
  • when 절에서는 비즈니스 로직을 실행시키고 예외가 발생하더라도 latch의 개수를 감소시켜 개수가 몇 번 실행되었는지 확인한다.
  • then 절에서는 원하는 결괏값이 나왔는지 확인하는 코드이다.



2. 잘 동작할까?

그러면 좋겠지만, 문제가 발생했다.

 

2.1 내부 트랜잭션 문제

ExecutorService안에서는 트랜잭션이 동작하지 않는다. finishReview()안에 TransactionSynchronizationManager.isActualTransactionActive()를 이용해서 로그를 찍어보면 ExecutorService 안에서 트랜잭션이 동작하지 않는다. 정확한 이유는 모르겠지만, @DataJpaTest에 @Transactional 메서드가 달려있어서 service에서는 동작하지만, 새로운 쓰레드여서 Transaction이 적용 안 되는 것 같다.

이 문제를 해결하기 위해서 아래와 같이 TransactionTemplate를 사용해서 직접 트랜잭션을 실행시켜 줬다.

transactionTemplate.execute((none) -> {  
    try {  
       runnerPostDeadlineCheckScheduler.finishReview(runnerPost.getId());  
    } catch (final Exception e) {  
    } finally {  
        latch.countDown();  
    }  
    return null;  
})

 

2.2 given 절의 데이터 초기 세팅 문제

그 다음 테스트를 실행시켜보자.

오류가 또 발생했다. 왜 그런걸까?
원인은 트랜잭션 격리 수준 때문이다.

Tx1에서 save 메서드로 미리 RunnerPost를 저장한다. 그 후 ExcutorService로 finishReview()를 호출할 때 RunnerPost를 찾는다. 정상적으로 저장한 뒤 찾는 로직인 것 같지만, 쓰레드가 분리되어, 트랜잭션이 나뉜 게 문제가 된다. 보통 데이터베이스들은 트랜잭션 격리 수준이 Read Commited 이상(h2 : Read Commited, MySQL : Repeatable Read)이다. 이 때문에 트랜잭션에서의 변경 사항은 commit이 되어 있지 않으면 다른 트랜잭션에서 읽을 수 없다.
문제를 해결하기 위해서 데이터베이스에 강제로 커밋을 해줘야 하는데, 나는 TestTransaction 클래스를 사용해 트랜잭션을 수동으로 관리해 줬다.

private void transactionCommit() {  
    TestTransaction.flagForCommit();  
    TestTransaction.end();    
}

이 메서드를 통해 트랜잭션 커밋을 강제로 발생하게 했다.

최종적인 코드는 다음과 같고, 성공하는 테스트를 볼 수 있었다.

@Test  
void finishReview_when_multiple_request() throws InterruptedException {  
    // given  
    final Runner runner = persistRunner(MemberFixture.createHyena());  
    final RunnerPost runnerPost = persistRunnerPost(runner);  
    final Supporter supporter = persistSupporter(MemberFixture.createEthan());  
    persistAssignSupporter(supporter, runnerPost);  
    deadlineOutboxCommandRepository.save(DeadlineOutboxFixture.deadlineOutbox(runnerPost.getId(), Instant.now()));  

    transactionCommit();  

    final ReviewCount originReviewCount = ReviewCountFixture.reviewCount(supporter.getReviewCount().getValue());  
    final int requests = 10;  
    final ExecutorService executorService = Executors.newFixedThreadPool(requests);  
    final CountDownLatch latch = new CountDownLatch(requests);  

    // when  
    for (int i = 0; i < requests; i++) {  
        executorService.submit(() ->  
                transactionTemplate.execute((none) -> {  
                    try {  
                        runnerPostDeadlineCheckScheduler.finishReview(runnerPost.getId());  
                    } catch (final Exception e) {  
                        e.printStackTrace();  
                    } finally {  
                        latch.countDown();  
                    }  
                    return null;  
                })  
        );  
    }  

    latch.await();  
    executorService.shutdown();  

    // then  
    final RunnerPost actual = runnerPostQueryRepository.joinSupporterByRunnerPostId(runnerPost.getId()).get();  
    assertAll(  
            () -> assertThat(actual.getSupporter().getReviewCount()).isEqualTo(ReviewCountFixture.reviewCount(originReviewCount.getValue() + 1)),  
            () -> assertThat(actual.isReviewStatusDone()).isTrue(),  
            () -> assertThat(deadlineOutboxCommandRepository.findAll()).isEmpty()  
    );  
}  

private void transactionCommit() {  
    TestTransaction.flagForCommit();  
    TestTransaction.end();  
}




※ 참고

  • A Guide to the Java ExecutorService
저작자표시 (새창열림)

'개발 > [우테코]' 카테고리의 다른 글

확장을 고려해서 TaskScheduler로 자동 리뷰 완료 기능 구현하기  (0) 2024.02.20
Spring에서 Redis Test 하기  (0) 2024.02.12
Redis를 활용해서 RefreshToken 최적화하기  (0) 2024.02.08
바톤의 DB Replication  (0) 2023.10.14
build 할 때 Rest Docs 파일이 생성이 안되는 문제 트러블 슈팅  (0) 2023.08.20
  1. 0. 들어가기 전에
  2. 1. 어떻게 테스트 할까?
  3. 2. 잘 동작할까?
  4. 2.1 내부 트랜잭션 문제
  5. 2.2 given 절의 데이터 초기 세팅 문제
  6. ※ 참고
'개발/[우테코]' 카테고리의 다른 글
  • 확장을 고려해서 TaskScheduler로 자동 리뷰 완료 기능 구현하기
  • Spring에서 Redis Test 하기
  • Redis를 활용해서 RefreshToken 최적화하기
  • 바톤의 DB Replication
쿠엔크
쿠엔크
우아한테크코스 5기 BE 에단 Github : https://github.com/cookienc
쿠엔크
기러기는 기록기록
쿠엔크
전체
오늘
어제
  • 분류 전체보기 (132)
    • CS (46)
      • [OS] (12)
      • [NETWORK] (10)
      • [DATABASE] (11)
      • [BASIC CONCEPT] (1)
      • [DATA STRUCTURE] (7)
      • [ALGORITHM] (5)
    • LANGUAGE (17)
      • [JAVA] (17)
    • DESIGN_PATTERN (2)
    • FRAMEWORK (18)
      • [SPRING] (18)
    • ORM (11)
      • JPA (11)
    • AWS (7)
    • BOOK (10)
      • [자바 웹 개발 워크북] (3)
      • [이펙티브 자바] (7)
    • 개발 (19)
      • [오류] (7)
      • [고민] (1)
      • [우테코] (10)
      • [iTracker] (1)
    • Tip (1)
      • [Plugins] (1)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • 오류
  • java
  • JVM
  • CORS
  • 데이터베이스
  • 네트워크
  • aws
  • 디자인 패턴
  • HTTP
  • Spring
  • 운영체제
  • 스프링
  • JPA
  • 알고리즘
  • Effective Java
  • 자바 웹 개발 워크북
  • 개념
  • 가비아
  • 자료구조
  • ArgumentResolver

최근 댓글

최근 글

hELLO · Designed By 정상우.v4.2.2
쿠엔크
락을 사용한 동시성 테스트하기

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.