코틀린 환경에서 Async 어노테이션 테스트하기
비즈니스 로직을 처리하다보면 비동기처리를 하고 싶을 때가 있다. 여러가지 방법이 있지만 스프링에서는 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 메소드만 비동기 처리가 가능하다는 것.
이제 진짜 테스트 코드를 만들어보자.
@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용 코드도 작성되어 있지만 본 컨텐츠에서는 다루지 않는다. 하지만 동작이 되는 코드이니 공부할 때 참고할 수 있다.
🍖 참고자료
- @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