728x90
반응형

[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 반환 + 별도 빈 분리가 기본


728x90
반응형

+ Recent posts