Spring

코틀린 환경에서 Async 어노테이션 테스트하기

미스터머글 2022. 2. 8. 16:19
728x90
반응형

 

비즈니스 로직을 처리하다보면 비동기처리를 하고 싶을 때가 있다. 여러가지 방법이 있지만 스프링에서는 Async 어노테이션을 사용하여 비동기 처리가 가능하다. 하지만 Async를 바로 적용하기 보다 테스트를 먼저 해보고 싶다는 생각이 들 수 있다. 동작 방식 등을 이해하고 싶다거나 비즈니스 로직이 비동기 처리에 적합한지 확인하기 위해서 테스트에서 Async를 사용하고 싶을 수 있다. 그래서 테스트에서 Async를 어떻게 적용할 수 있는지 설명해보겠다. 아래에 공개되는 모든 코드는 필자의 깃헙에도 있으니 참고하시면 되겠다.

 

⚠️ 경고 ⚠️
여기서는 다음과 같은 내용은 자세히 다루지 않습니다. 별도로 찾아보시길 권합니다.
> 동기/비동기, 쓰레드, 멀티쓰레드 등

 

🍿 @Async란

스프링 프레임워크에서 제공하는 어노테이션으로 Thread pool을 활용한 비동기 처리를 지원한다. 동작 방식 등은 뒤에 설명하겠다.

 

 

🍿 선행작업 feat Executor

스프링부트를 사용한다는 가정하에서 설명을 진행하겠다. 원래라면 아래처럼 Application 클래스에 @EnableAsync를 추가해줘야 한다. 이 상태에서 Async 어노테이션을 사용하면 기본세팅인 SimpleAsyncTaskExecutor가 적용된다.

@SpringBootApplication
@EnableAsync
class SampleApplication

 

Executor란 작업을 비동기처리로 처리하도록 지원하는 클래스로 쓰레드 풀을 운영한다. 보통 TaskExecutor 인터페이스를 사용하며 이는 Task를 받고 execute 메서드를 실행한다. 여러 TaskExecutor 인터페이스 중 위에서 언급된 SimpleAsyncTaskExecutor는 어떤 스레드도 재사용하지 않고 호출할 때마다 새로운 스레드를 시작한다. 하지만 기본 쓰레드 풀 사이즈나 최대 사이즈등을 원하는대로 변경하고 싶을 수 있다. 그럴 때는 아래와 같이 Configuration을 추가해주면 된다.

 

@Configuration
@EnableAsync
class AsyncConfig(
    @Value("\${sample.poolsize}")
    private val poolSize: Int,
) {
    @Bean
    fun taskExecutor(): ThreadPoolTaskExecutor {
        val taskExecutor = ThreadPoolTaskExecutor()

        taskExecutor.setThreadNamePrefix("QueueTask-")
        taskExecutor.corePoolSize = poolSize
        taskExecutor.maxPoolSize = poolSize * 2
        taskExecutor.setQueueCapacity(poolSize * 5)
        taskExecutor.setTaskDecorator(LoggingTaskDecorator())
        taskExecutor.setWaitForTasksToCompleteOnShutdown(true)

        return taskExecutor
    }
}

 

sample.poolsize 부분은 application.yml을, LoggingTaskDecorator는 AsyncConfig 코드 아래쪽을 참고하면 된다. 위처럼 작성하면 원하는 TaskExecutor 설정으로 비동기 처리를 할 수 있다. 이 때 주의할 것은 기존 Application 클래스에 있던 EnableAsync은 제거하고 AsyncConfig 클래스에 붙여줘야 한다는 점이다.

 

 

🍿 테스트를 해보자

먼저 테스트 클래스와 Sample 클래스를 준비하자. Sample 클래스는 빈(Bean)으로 등록해서 사용해야 한다. 그리고 비동기 처리할 메소드에는 Async 어노테이션을 붙이고 AsyncConfig에서 빈으로 등록한 메서드명을 Async의 프로퍼티에 넣어주자. 비교를 해 보기 위해 동기 처리용 메소드도 추가해보자.

@SpringBootTest
class SampleApplicationTests {

    @Autowired
    lateinit var a: Sample

}

@Component
class Sample {
    @Async("taskExecutor")
    fun async() {
        Thread.sleep(300)
        println("""
            In async: ${Thread.currentThread()} // ${LocalTime.now()}
        """.trimIndent())
    }

    fun sync() {
        Thread.sleep(100)
        println("""
            In sync: ${Thread.currentThread()} // ${LocalTime.now()}
        """.trimIndent())
    }
}

 

"테스트 클래스 내부에서 Async 메소드를 만들면 되지 않냐"라고 물을 수 있는데 그렇게 사용하면 비동기 처리가 되지 않는다. 프록시를 생성하더라도 쓰레드를 생성하지 않고 해당 메서드를 직접 호출하기 때문이다. 자세한 사항은 아래 컨텐츠의 "Limitations of @Async" 파트에서 3번 문단을 참고하길 바란다. 결론은 다른 클래스에 있는 Async 메소드만 비동기 처리가 가능하다는 것.

 

 

Effective Advice on Spring Async: Part 1 - DZone Java

In this post, we explore some of the biggest misconceptions and limitations when working with Spring's Async annotation.

dzone.com

 

이제 진짜 테스트 코드를 만들어보자.

@Test
fun test() {
    println("Before, in test: " + Thread.currentThread())
    for (i in 0..5) {
        a.async()
    }

    for (i in 0..5) {
        a.sync()
    }
    println("After, in test: " + Thread.currentThread())
}

 

비동기 메서드를 먼저 호출하고 동기 메서드를 호출해 보았다. 비동기 메서드의 0.3초 딜레이 때문에 동기 메서드가 동작 중에 비동기 처리가 발생하는 것을 확인할 수 있다. 또한 비동기 처리 쓰레드와 동기 처리 쓰레드가 어떻게 다른지도 확인할 수 있도록 로그를 찍어보았다.

 

// 결과
Before, in test: Thread[Test worker,5,main]
In sync: Thread[Test worker,5,main] // 18:23:31.521266
In sync: Thread[Test worker,5,main] // 18:23:31.635759
In async: Thread[QueueTask-1,5,main] // 18:23:31.718729
In async: Thread[QueueTask-2,5,main] // 18:23:31.718752
In async: Thread[QueueTask-4,5,main] // 18:23:31.718747
In async: Thread[QueueTask-6,5,main] // 18:23:31.718730
In async: Thread[QueueTask-3,5,main] // 18:23:31.718746
In async: Thread[QueueTask-5,5,main] // 18:23:31.718743
In sync: Thread[Test worker,5,main] // 18:23:31.739659
In sync: Thread[Test worker,5,main] // 18:23:31.844277
In sync: Thread[Test worker,5,main] // 18:23:31.948230
In sync: Thread[Test worker,5,main] // 18:23:32.053757
After, in test: Thread[Test worker,5,main]

 

Test worker는 테스크 코드 내부의 메인 쓰레드이며 동기 처리를 할 때 동작하는 것을 볼 수 있다. 동기처리가 되었는지는 처리된 로그가 0.1초 차이가 나는 것으로 확인이 가능하다. 비동기 처리에서는 여러개의 쓰레드가 거의 동시에 동작하는 것을 볼 수 있다.

 

이렇게 테스트 코드로 Async 어노테이션을 사용해 보았다. 아래 깃헙을 통해 전체 코드를 확인할 수 있다. 해당 깃헙에는 Future용 코드도 작성되어 있지만 본 컨텐츠에서는 다루지 않는다. 하지만 동작이 되는 코드이니 공부할 때 참고할 수 있다.

 

 

GitHub - conquerex/WhatTheJpaBook: JPA 스터디 실습 예제

JPA 스터디 실습 예제. Contribute to conquerex/WhatTheJpaBook development by creating an account on GitHub.

github.com

 

🍖 참고자료
- @Async Annotation(비동기 메소드 사용하기)
- Spring @Async 비동기처리
- Spring @Async Annotation을 활용한 Thread 구현
- Java 동시성(Concurrency) Threads and Executors
- Effective Advice on Spring Async: Part 1
🍖 Future 공부할 때 참고자료
- Future 사용 방법
- CompletableFuture 사용 방법
- Java Future
728x90
반응형