-
Kotlin 동시성 테스트하기프로젝트/미디어 스트리밍 서버 프로젝트 2022. 10. 7. 00:01
개요
Multi Thread 환경에서 데이터의 정합성을 보장할 수 있는지 확인하고자 합니다.
코틀린 공식문서를 기반으로 학습해 보겠습니다.
https://kotlinlang.org/docs/multiplatform-mobile-concurrency-overview.html
규칙 1 : 변경 가능한 상태라면 1개의 스레드에서 동작해야 합니다.
data class SomeData(var count:Int) fun simpleState(){ val sd = SomeData(42) sd.count++ println("My count is ${sd.count}") // It will be 43 }
위의 메서드 처럼 상태를 가지는 경우에는 스레드가 1개일 경우에 동시성 문제가 발생하지 않습니다.
규칙 2 : 변경이 불가능한 상태라면 여러개의 스레드에서 동작할 수 있습니다.
val을 통해서 불변 객체를 만드려고 하지만 객체의 경우에는 내부적으로 변경 가능할 수 있습니다.
따라서 freeze() 메서드를 제공함으로써 불변성을 조금 더 지원합니다.
또한 @ThreadLocal, @SharedImmutable 등을 활용하여 Object, companion ojbect에서 공유 변수를 사용할 수 있습니다.
하지만 상태를 가지더라도 동시성이 필요할 수 있습니다.
규칙1,2에 따르면 상태를 가지면 1개의 스레드에서 동작해야 하지만 여러 개의 스레드에서 동작해야 하는 경우도 존재합니다.
이때 사용할 수 있는 다양한 기술들이 있습니다.
- Atomics
- Thread-isolated states
- Low-level capabilities
동시성을 활용해서 테스트 작성해보기
자바에서는 ExecutorService를 활용해서 멀티 쓰레드 환경을 구축할 수 있었습니다.
코틀린에서는 코루틴을 사용할 수 있습니다.
class CoroutinesTutorial { var counter = 0 @Test fun coroutinesTest() { runBlocking { GlobalScope.massiveRun { counter++ } println("Counter = $counter") //100 * 1000번인 100_000이 아닌 54486이 출력됨 공유 변수를 다루게 되면 동시성 문제 } } } suspend fun CoroutineScope.massiveRun(action: suspend () -> Unit) { val n = 100 // number of coroutines to launch val k = 1000 // times an action is repeated by each coroutine val time = measureTimeMillis { val jobs = List(n) { launch { repeat(k) { action() } } } jobs.forEach { it.join() } } println("Completed ${n * k} actions in $time ms") }
공유 변수인 counter를 증가시키는 연산을 수행합니다.
이때 100_000번 수행하기 때문에 100_000이 예상되지만 동시성 문제로 훨씬 적은 값이 출력됩니다.
그러면 이 점을 활용해서 DB에 여러 요청이 동시에 들어왔을 때 원하는 대로 동작하는지 확인해보겠습니다.
조회수를 1증가시키는 메서드를 테스트해보겠습니다.
Service
fun plusHitCount(hitId: Long) { videoRepository.plusHitCountById(hitId) }
Reposiotry
@Modifying @Query(value = "update Hit t set t.hitCount = t.hitCount + 1 where t.id =:hitId") fun plusHitCountById(hitId: Long)
Test 코드
@Test @DisplayName("동시성 환경에서 조회수 증가 정합성이 보장되어야 한다") @Trasnactional fun multiThreadUpdateTest() { //given val registerVideo = videoService.registerVideo( UploadRequest( "subject", "content", ) ) //when runBlocking { GlobalScope.massiveUpdate { videoService.plusHitCount(registerVideo.id) } } videoRepository.flush() entityManager.clear() println("db processing 끝") //then val video = videoService.findSingleVideo(registerVideo.id) val count = video.hit.hitCount Assertions.assertThat(count).isEqualTo(10*100) } suspend fun CoroutineScope.massiveUpdate(action: suspend () -> Unit){ val n = 10 // number of coroutines to launch val k = 100 // times an action is repeated by each coroutine val time = measureTimeMillis { val jobs = List(n) { launch { repeat(k) { action() } } } jobs.forEach { it.join() } } println("Completed ${n * k} actions in $time ms") }
1. 비디오를 등록합니다.
2. 코루틴을 활용하여 1000번의 업데이트를 실행시킵니다.
3. 비디오를 조회해서 조회수가 1000인지 검증합니다.
Exception 발생
위의 테스트를 실행시키면 time out exception이 발생합니다.
HikariPool-1 - Connection is not available, request timed out after 30013ms.
Caused by: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction
Caused by: org.hibernate.PessimisticLockException: could not execute statement
이를 해결하기 위해 여러 가지 방법들을 시도해 보았습니다.
1. 코루틴의 문제일 수 있다고 생각하여 ExecutorService를 사용해 보았습니다.
2. 테스트시 h2 db를 사용하고 있는데 Mysql과 차이점이 있을 수 있을 것 같아 MySql으로 테스트해보았습니다.
3. update query가 나가므로 DeadLock이 발생할 수 있겠다고 생각하여 Hikari의 Connection Pool 설정을 변경해보고, Lock Time out 등의 설정을 해주어 보았습니다.
4. 동시 요청 수가 너무 많을 수 있다고 생각하여 요청 수를 점점 줄여 보았습니다. 하지만 100개의 요청을 보내던 2개의 요청을 보내던 동일한 에러가 발생했습니다.
5. update query가 문제일 수 있다고 생각하여 select query를 날려보았습니다. 이때에는 db에서 값을 제대로 조회해오지 못해서 null이 들어와 Exception이 발생했습니다.
Exception in thread "DefaultDispatcher-worker-3 @coroutine#8" java.lang.IllegalArgumentException
이때부터 무언가 근본적으로 잘못되었음을 깨달았지만 무엇이 문제인지 알 수 없었습니다.
원인은 @Transactionl 어노테이션 때문이였습니다.
test 클래스에 선언된 트랜잭션 때문에 여러 개의 요청이 하나의 트랜잭션에서 처리되어 lock을 계속 점유하고 있어서 발생한 일이었습니다.
@Transactional 어노테이션을 제거한 후 테스트는 정상적으로 동작하여 1000건의 동시 요청에 대해 조회수가 1000 증가하는 것을 검증할 수 있었습니다.
출처
https://tourspace.tistory.com/163
'프로젝트 > 미디어 스트리밍 서버 프로젝트' 카테고리의 다른 글
OTT는 어떻게 콘텐츠를 보호할까? (0) 2022.12.08 리팩토링에 대하여 (0) 2022.10.08 JPA 연관관계 Paging 최적화 (0) 2022.10.06 @TestConfiguration 설정하기 (0) 2022.10.05 @Transactional 롤백과 @TransactionalEventListener (0) 2022.10.04