비즈니스 로직을 처리하다보면 비동기처리를 하고 싶을 때가 있다. 여러가지 방법이 있지만 스프링에서는 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
'Spring' 카테고리의 다른 글
data.sql이 동작하지 않을 때, 의심해봐야 할 것 (0) | 2022.07.13 |
---|---|
Google Oauth - Token 획득하기 (Signup/Signin용) (0) | 2022.03.30 |
Kotlin에서 RestTemplateClient를 만들어보자 (0) | 2021.12.23 |
미해결사건. @Valid가 작동하지 않는다?? (0) | 2021.12.15 |
스프링 게이트웨이에서 dev, prod 등 여러 환경 적용하기 (0) | 2021.10.07 |
Comment