1. 🍿 @Async란
2. 🍿 선행작업 feat Executor
3. 🍿 테스트를 해보자

비즈니스 로직을 처리하다보면 비동기처리를 하고 싶을 때가 있다. 여러가지 방법이 있지만 스프링에서는 Async 어노테이션을 사용하여 비동기 처리가 가능하다. 하지만 Async를 바로 적용하기 보다 테스트를 먼저 해보고 싶다는 생각이 들 수 있다. 동작 방식 등을 이해하고 싶다거나 비즈니스 로직이 비동기 처리에 적합한지 확인하기 위해서 테스트에서 Async를 사용하고 싶을 수 있다. 그래서 테스트에서 Async를 어떻게 적용할 수 있는지 설명해보겠다. 아래에 공개되는 모든 코드는 필자의 깃헙에도 있으니 참고하시면 되겠다.
⚠️ 경고 ⚠️
여기서는 다음과 같은 내용은 자세히 다루지 않습니다. 별도로 찾아보시길 권합니다.
> 동기/비동기, 쓰레드, 멀티쓰레드 등
1. 🍿 @Async란
스프링 프레임워크에서 제공하는 어노테이션으로 Thread pool을 활용한 비동기 처리를 지원한다. 동작 방식 등은 뒤에 설명하겠다.
2. 🍿 선행작업 feat Executor
스프링부트를 사용한다는 가정하에서 설명을 진행하겠다. 원래라면 아래처럼 Application 클래스에 @EnableAsync를 추가해줘야 한다. 이 상태에서 Async 어노테이션을 사용하면 기본세팅인 SimpleAsyncTaskExecutor가 적용된다.
@SpringBootApplication @EnableAsync class SampleApplication
kotlin
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 } }
kotlin
sample.poolsize 부분은 application.yml을, LoggingTaskDecorator는 AsyncConfig 코드 아래쪽을 참고하면 된다. 위처럼 작성하면 원하는 TaskExecutor 설정으로 비동기 처리를 할 수 있다. 이 때 주의할 것은 기존 Application 클래스에 있던 EnableAsync은 제거하고 AsyncConfig 클래스에 붙여줘야 한다는 점이다.
3. 🍿 테스트를 해보자
먼저 테스트 클래스와 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()) } }
kotlin
"테스트 클래스 내부에서 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()) }
kotlin
비동기 메서드를 먼저 호출하고 동기 메서드를 호출해 보았다. 비동기 메서드의 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]
shell
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
'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