본문으로 건너뛰기
개발 머꼬
개발 노트Spring
hohyeon.dev22

@Async를 붙였는데 스레드가 하나씩만 처리되는 이유

  • #Concurrency
  • #Engineering Note
  • #Spring

문제 발생

무거운 작업을 비동기로 처리하려고 @Async를 붙였는데, 트래픽이 몰리자 서버가 스레드를 무제한으로 만들어내며 리소스가 고갈되는 문제가 발생했습니다.

@Async
public void sendNotification(User user) {
    // 오래 걸리는 작업
}

원인 분석

@EnableAsync만 켜고 별도로 Executor 빈을 설정하지 않으면, Spring은 기본값으로 SimpleAsyncTaskExecutor를 씁니다. 이름 그대로 "단순한" 구현인데, 스레드 풀을 재사용하지 않고 호출될 때마다 매번 새 스레드를 생성합니다. 동시 호출이 적을 때는 문제가 없어 보이지만, 트래픽이 몰리면 스레드 수가 제한 없이 계속 늘어나 메모리와 컨텍스트 스위칭 비용이 폭증하고, 결국 OutOfMemoryError나 시스템 전체 성능 저하로 이어질 수 있습니다.

해결 방안

  1. 전용 ThreadPoolTaskExecutor를 직접 정의하고 크기 제한을 명시적으로 둡니다.
@Configuration
@EnableAsync
public class AsyncConfig {
    @Bean(name = "notificationExecutor")
    public Executor notificationExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(20);
        executor.setQueueCapacity(100); // 큐가 다 차면 이후 정책(거부 등) 필요
        executor.setThreadNamePrefix("notif-");
        executor.initialize();
        return executor;
    }
}
  1. @Async에 사용할 Executor 이름을 명시합니다 — 지정하지 않으면 다시 기본값으로 돌아갑니다.
@Async("notificationExecutor")
public void sendNotification(User user) { ... }
  1. 큐가 가득 찼을 때의 정책(거절, 호출 스레드에서 직접 실행 등)도 함께 고려해야 합니다 — 무작정 무한 대기하게 두면 다른 형태의 자원 고갈로 이어질 수 있습니다.
  2. @Async 메서드 안에서 발생한 예외는 호출한 쪽으로 전파되지 않습니다 — void 반환 메서드라면 AsyncUncaughtExceptionHandler를 별도로 등록해야 예외를 로깅이라도 할 수 있습니다. 기본 설정으로는 예외가 조용히 사라져 디버깅이 어려워집니다.
  3. @Async 메서드를 같은 클래스 안에서 직접 호출(self-invocation)하면 프록시를 거치지 않아 비동기 실행 자체가 안 되는 함정도 있습니다 — 이건 @Transactional의 self-invocation 문제와 같은 원인(Spring AOP 프록시 메커니즘)입니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.