[Spring] @Async 동작하지 않는 경우
환경 : Spring Boot 3.x, Kotlin 1.9
1. 원인
@EnableAsync설정 누락- 같은 클래스 내부에서 호출 (프록시 우회)
private메서드에 적용void반환 타입 사용 — 예외 추적 불가- 같은 스레드풀에서 호출자·피호출자 동시 사용 시 데드락
2. 해결 방법
2.1) @EnableAsync 추가
@Transactional과 마찬가지로 명시적으로 활성화 필요
@EnableAsync
@SpringBootApplication
class Application
fun main(args: Array<String>) {
runApplication<Application>(*args)
}
2.2) 자기호출 회피 — 별도 빈으로 분리
Spring AOP 프록시 기반이라 같은 클래스 내부 호출 시 @Async 무시됨
// 동작 안 함
@Service
class OrderService(private val orderRepository: OrderRepository) {
fun process(orderId: Long) {
val order = orderRepository.findById(orderId).orElseThrow()
sendEmail(order) // 내부 호출 → @Async 무시
}
@Async
fun sendEmail(order: Order) {
println("스레드: ${Thread.currentThread().name}")
emailService.send(order.email, "주문 완료")
}
}
// 정상 — 별도 빈 분리
@Service
class OrderService(private val asyncEmailService: AsyncEmailService) {
fun process(orderId: Long) {
val order = orderRepository.findById(orderId).orElseThrow()
asyncEmailService.sendEmail(order) // 외부 빈 호출
}
}
@Service
class AsyncEmailService(private val emailService: EmailService) {
@Async
fun sendEmail(order: Order) {
println("스레드: ${Thread.currentThread().name}")
emailService.send(order.email, "주문 완료")
}
}
2.3) public 메서드 사용
private, protected 메서드는 프록시가 가로채지 못함
// 동작 안 함
@Service
class NotificationService {
@Async
private fun send(message: String) { ... } // private 무시
}
// 정상
@Service
class NotificationService {
@Async
fun send(message: String) { ... }
}
2.4) 반환 타입 — CompletableFuture 사용
void(Kotlin에서는 Unit)는 결과나 예외를 받을 수 없음. 비동기 메서드가 실패해도 호출자는 모름
// 나쁜 예 — 예외 발생해도 추적 불가
@Async
fun processOrder(orderId: Long) {
val order = orderRepository.findById(orderId).orElseThrow()
throw RuntimeException("결제 실패") // 로그에만 찍히고 끝
}
// 좋은 예 — CompletableFuture 반환
@Async
fun processOrder(orderId: Long): CompletableFuture<Order> {
val order = orderRepository.findById(orderId).orElseThrow()
paymentService.charge(order)
return CompletableFuture.completedFuture(order)
}
// 호출자에서 예외 처리 가능
fun process(orderId: Long) {
asyncService.processOrder(orderId)
.exceptionally { ex ->
log.error("비동기 실패: ${ex.message}")
null
}
}
2.5) TaskExecutor 분리 — 데드락 방지
기본 스레드풀은 SimpleAsyncTaskExecutor — 매번 새 스레드 생성. 운영 환경에서는 풀 크기 제한된 Executor 직접 설정 권장
@Configuration
@EnableAsync
class AsyncConfig {
@Bean(name = ["taskExecutor"])
fun taskExecutor(): Executor {
val executor = ThreadPoolTaskExecutor()
executor.corePoolSize = 5
executor.maxPoolSize = 10
executor.queueCapacity = 100
executor.setThreadNamePrefix("async-")
executor.initialize()
return executor
}
}
여러 Executor를 나눠서 사용 가능
@Configuration
@EnableAsync
class AsyncConfig {
@Bean(name = ["emailExecutor"])
fun emailExecutor(): Executor {
val executor = ThreadPoolTaskExecutor()
executor.corePoolSize = 3
executor.maxPoolSize = 5
executor.setThreadNamePrefix("email-")
executor.initialize()
return executor
}
@Bean(name = ["reportExecutor"])
fun reportExecutor(): Executor {
val executor = ThreadPoolTaskExecutor()
executor.corePoolSize = 2
executor.maxPoolSize = 3
executor.setThreadNamePrefix("report-")
executor.initialize()
return executor
}
}
@Service
class NotificationService {
@Async("emailExecutor")
fun sendEmail(message: String) { ... }
@Async("reportExecutor")
fun generateReport(userId: Long) { ... }
}
주의 — 같은 스레드풀에서 A가 B를 호출하고 B도 비동기면, 스레드풀이 꽉 차면 데드락 발생 가능. 이런 경우 호출 구조에 따라 Executor를 나눠야 함
3. 디버깅
3.1) 스레드 이름 로그
비동기가 제대로 동작하는지 확인하려면 스레드 이름 출력
@Service
class OrderService {
fun process(orderId: Long) {
println("호출 스레드: ${Thread.currentThread().name}") // http-nio-8080-exec-1
asyncService.sendEmail(orderId)
}
}
@Service
class AsyncEmailService {
@Async
fun sendEmail(orderId: Long) {
println("비동기 스레드: ${Thread.currentThread().name}") // async-1
emailService.send(...)
}
}
같은 스레드 이름이 나오면 @Async가 무시된 것
3.2) 로그 레벨 설정
# application.yml
logging:
level:
org.springframework.aop.interceptor.AsyncExecutionInterceptor: TRACE
정상 동작 시 출력
Async execution of method [...] started
Async execution of method [...] finished
※ 자기호출과 void 반환 타입이 가장 흔한 실수. 실무에서는 CompletableFuture 반환 + 별도 빈 분리가 기본
'TroubleShooting' 카테고리의 다른 글
| [JPA] save()가 INSERT 대신 SELECT를 날리는 경우 (0) | 2026.04.15 |
|---|---|
| [JPA] MultipleBagFetchException (0) | 2026.04.04 |
| [vue] CORS 해결 (1) | 2022.06.19 |
| [Vue] You are using the runtime-only build of Vue where the template compiler is not available. (0) | 2020.09.07 |
| [IntelliJ] Git 관련 작업 시 hangs on/Holding (0) | 2020.06.16 |
