Backend/SpringBoot

@Async 설정 및 사용

Dean83 2026. 1. 2. 11:58

@Async 를 사용하기 위해, @EnableAsync 어노테이션을 Configuration 클래스에 붙여준다. 

 

Configuration 설정

  • TaskExecutor 설정을 Bean으로 한다. 
  • 다수의 TaskExecutor 를 설정하고, Async 어노테이션에서 골라서 실행 할 수 있다.
    • cpu를 많이 사용하는 작업인지, I/O 작업이 많은지 등에 따라 설정 하고 골라 사용 가능. 
  • 혹은, application.yaml 에서도 가능하다.
@Configuration
@EnableAsync
public class AsyncConfig {

    /**
     * 가벼운 비동기 작업용
     * (알림, 로그, 이벤트 후처리 등)
     */
    @Bean(name = "lightTaskExecutor")
    public Executor lightTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(200);
        executor.setThreadNamePrefix("async-light-");
        executor.initialize();
        return executor;
    }

    /**
     * 무거운 비동기 작업용
     * (외부 API 호출, 파일 처리, 배치성 작업 등)
     */
    @Bean(name = "heavyTaskExecutor")
    public Executor heavyTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(20);
        executor.setQueueCapacity(50);
        executor.setThreadNamePrefix("async-heavy-");
        executor.initialize();
        return executor;
    }
}
  • SimpleAsyncTaskExecutor, ThreadPoolTaskExecutor 등이 있다. 
    • ThreadPoolTaskExecutor 사용을 권장한다.
      • setCorePoolSize : 기본적으로 유지되는 스레드 수
      • maxPoolSize : 최대 생성 가능한 스레드 수
      • setQueueCapacity : 작업대기 큐 개수
      • keepAliveSeconds : 유휴 스레드 유지시간 설정. 
      • threadNamePrefix : 스레드 이름 식별을 위한 접두사

 

기본 Default 용 Async 설정 (AsyncConfigurer)

  • @Async 에서 특정 Executor를 지정하지 않을경우, 이 설정에서 명시한 Executor가 동작하게 된다.
    • 이 설정없이 @Async 사용시 Executor를 지정하지 않으면, 그냥 비동기 동작한다. Executor를 사용하지 않는다.
  • Exception 관련 처리도 할 수 있다. 
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

        executor.setCorePoolSize(5);      // 기본 스레드
        executor.setMaxPoolSize(10);      // 최대 스레드
        executor.setQueueCapacity(100);   // 대기 큐
        executor.setThreadNamePrefix("async-");
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setAwaitTerminationSeconds(10);

        executor.initialize();
        return executor;
    }

    @Override
    public AsyncUncaughtExceptionHandler
    getAsyncUncaughtExceptionHandler() {
        return new GlobalAsyncExceptionHandler();
    }
}

 

@Async 를 이용한 비동기 정의

  • 메서드, 혹은 클래스에 붙일 수 있는 어노테이션으로, 비동기로 동작함을 정의할때 사용한다. 
    • 클래스에 붙이면 하위 모든 동작이 비동기
    • 메서드에 붙이면 해당 메서드만 비동기
  • 주의점 : 같은 클래스 내 에서 동기, 비동기 메서드가 별개로 있는 상황에서,  동기 메서드에서 비동기 메서드를 호출해도 비동기 동작하지 않는다. 
    • 외부 -> 빈 주입 -> 주입된 빈의 비동기 메서드 호출시, proxy를 통해 실행되므로 비동기 작업이 수행된다.
    • 내부 -> 비동기 메서드 호출시, proxy를 통하지 않게 되므로 비동기 작업이 수행되지 않는다.
    • 따라서, 비동기 메서드들을 모아 별도의 서비스로 구현하고, 타 서비스에서 이 서비스를 이용하도록 하는것이 좋다. 
@Service
public class NotificationService {

    @Async("lightTaskExecutor")
    public void sendNotification(String message) {
        System.out.println(
            "light thread = " + Thread.currentThread().getName()
        );
    }
    
    @Async("heavyTaskExecutor")
    public void processFile(String filePath) {
        System.out.println(
            "heavy thread = " + Thread.currentThread().getName()
        );
    }
}
  • 대부분의 경우, Async 어노테이션이 붙은 메서드를 작성할때, 일반적인 코드 구현 방식으로 작성 하면 된다.
  • 만일 비동기 작업의 결과값을 반환받아 무언가 해야 한다면, CompletableFuture를 이용할 수도 있다.

 

트랜잭션 컨텍스트 관련

트랜잭션은 하나의 스레드에서만 동작하므로, 비동기 작업을 하게 되면 기존 트랜잭션은 이용할 수 없다. 

따라서, A 메서드에서 트랜잭션을 이용하여 작업을 하다가, B 메서드를 비동기 작업으로 호출하게 되면, 트랜잭션 전파가 되지 않는다. @Transactional(proparation = Propagation.REQURES_NEW) 어노테이션을 @Async와 같이 이용해야 한다.

(비동기 이벤트에서 한번 정리한 적이 있다. https://dean83.tistory.com/320)

 

비동기 작업시, 동시성 관련https://dean83.tistory.com/392 를 참조한다.

 

Actuator를 이용한 ThreadPool 모니터링

  • 프로메테우스 라이브러리를 추가 해야함
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    implementation 'io.micrometer:micrometer-registry-prometheus'
}
  • application.yaml 설정
management:
  endpoints:
    web:
      exposure:
        include: health,metrics,threaddump
  metrics:
    export:
      prometheus:
        enabled: true

 

  • 실시간으로 스레드풀 상태를 확인하기 위해 커스터마이징
package com.example.actuator;

import org.springframework.boot.actuator.endpoint.annotation.Endpoint;
import org.springframework.boot.actuator.endpoint.annotation.ReadOperation;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;

import java.util.Map;

@Component
@Endpoint(id = "threadpool")
public class ThreadPoolEndpoint {

    private final ThreadPoolTaskExecutor taskExecutor;

    public ThreadPoolEndpoint(ThreadPoolTaskExecutor taskExecutor) {
        this.taskExecutor = taskExecutor;
    }

    @ReadOperation
    public Map<String, Object> threadPoolInfo() {
        return Map.of(
            "activeCount", taskExecutor.getActiveCount(),
            "poolSize", taskExecutor.getPoolSize(),
            "corePoolSize", taskExecutor.getCorePoolSize(),
            "maxPoolSize", taskExecutor.getMaxPoolSize(),
            "queueSize", taskExecutor.getThreadPoolExecutor().getQueue().size()
        );
    }
}

 

혹은 scheduler를 이용하여 코드로 상태를 확인 할 수도 있다.  (스케줄러 사용 : https://dean83.tistory.com/373)