`@Async`는 스프링에서 비동기 작업을 처리할 때 사용하는 강력한 도구입니다.
비동기 작업을 통해 여러 작업을 동시에 실행하여 I/O가 오래 걸리거나 사용자 응답에 민감하지 않은 작업을 백그라운드에서 수행하게 함으로써, 시스템의 성능과 응답 속도를 향상할 수 있습니다.
이 글에서는 `@Async`와 스레드 풀 설정을 통해 비동기 작업을 어떻게 최적화할 수 있는지 알아보겠습니다.👩🏻💻
@Async란?
`@Async`는 스프링에서 메서드를 비동기적으로 실행하도록 지정할 때 사용하는 어노테이션입니다.
이 어노테이션을 적용하면 스프링은 별도의 스레드에서 해당 메서드를 실행하여 호출한 코드의 흐름이 메서드가 끝날 때까지 기다리지 않고 바로 다음 작업을 진행할 수 있습니다.
@Async 어노테이션 적용 방법
`@Async`를 사용하려면 먼저 스프링 비동기 기능을 활성화해야 합니다. `@EnableAsync`어노테이션을 애플리케이션의 설정 클래스에 추가하면 비동기 처리가 가능해집니다.
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableAsync
public class AsyncConfig {
// 추가적인 설정이 필요할 경우 여기에 작성
}
이제 `@Async`어노테이션을 메서드에 붙여 비동기 처리를 지정할 수 있습니다.
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class SampleService {
@Async
public void executeAsyncTask() {
System.out.println("비동기 작업 시작: " + Thread.currentThread().getName());
// 시간 소요 작업 수행
}
}
이제 `executeAsyncTask`메서드를 호출하면 메인 흐름을 방해하지 않고 백그라운드에서 비동기 작업이 수행됩니다.
@Async의 ThreadPool 설정
스프링에서는 `@Async`메서드의 기본 스레드 풀로 `SimpleAsyncTaskExecutor`를 사용하지만 대규모 애플리테이션에서는 별도로 스레드 풀 설정을 통해 성능을 최적화할 수 있습니다.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@Configuration
public class AsyncConfig {
@Bean(name = "customThreadPool")
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5); // 기본 스레드 수
executor.setMaxPoolSize(10); // 최대 스레드 수
executor.setQueueCapacity(25); // 큐 용량
executor.setThreadNamePrefix("AsyncExecutor-"); // 스레드 이름 접두사
executor.initialize();
return executor;
}
}
`ThreadPoolTaskExecutor`는 비동기 작업을 위한 커스텀 스레드 풀을 구성하는 데 사용됩니다. 기본적으로 다음과 같은 설정을 통해 스레드 풀의 동작 방식을 제어할 수 있습니다
- CorePoolSize: 스레드 풀이 생성하는 기본 스레드 수입니다. 요청이 들어오면 최소한 이 수의 스레드가 준비되어 작업을 처리합니다. 대기 중인 작업이 없다면 추가적인 스레드 생성이 이루어지지 않습니다.
- MaxPoolSize: 스레드 풀이 생성할 수 있는 최대 스레드 수입니다. 비동기 작업이 CorePoolSize 이상의 스레드를 요구할 경우, MaxPoolSize까지 스레드를 생성하여 작업을 병렬로 처리할 수 있습니다.
- QueueCapacity: 대기 중인 작업을 저장할 큐의 크기입니다. 스레드 풀이 처리할 수 있는 스레드 수를 초과하는 작업이 들어오면, 이 큐에 쌓이게 됩니다. 큐의 크기가 너무 작으면 추가 작업이 거부될 수 있고, 너무 크면 메모리를 많이 점유할 수 있습니다.
- ThreadNamePrefix: 디버깅이나 로깅 시 스레드 이름을 식별하기 위해 스레드 이름 앞에 추가할 접두사입니다. 특정 작업의 스레드를 구별할 때 유용합니다.
Tip: I/O 바운드 작업이 많은 경우 `CorePoolSize`를 크게 설정하는 것이 유리하며, CPU 바운드 작업에서는 `MaxPoolSize`를 CPU 코어 수에 맞추는 것이 좋습니다.
이제, 특정 메서드에서 커스텀 스레드 풀을 사용하고 싶다면 @Async("customThreadPool")을 지정하면 됩니다.
@Async("customThreadPool")
public void executeCustomAsyncTask() {
// 지정된 customThreadPool 스레드 풀에서 실행되는 비동기 작업
}
@Async의 내부 로직 이해하기
`@Async`가 어떻게 비동기 처리를 수행하는지 이해하면 사용 시 주의할 점을 더 잘 파악할 수 있습니다.
- 프록시 기반 비동기 처리
`@Async`는 AOP(Aspect-Oriented Programming) 프록시를 통해 비동기 처리를 구현합니다. `@Async`가 붙은 메서드가 호출되면 프록시가 이 메서드를 감싸 비동기 작업으로 변환됩니다.
프록시 패턴의 한계로 인해 private 메서드에서는 비동기 처리가 되지 않는 이유가 여기에 있습니다. - TaskExecutor로 작업 전달
프록시는 `@Async`메서드를 TaskExecutor에 작업으로 제출하여 비동기로 실행합니다. 이 작업은 Runnable형태로 전달되고 스레드 풀에 의해 관리됩니다.
기본적으로 스프링은 `SimpleAsyncTaskExecutor`를 사용하지만 앞서 설명한 것처럼 커스텀 스레드 풀을 설정할 수 있습니다, - AsyncAnnotationBeanPostProcessor
`@Async`는 스프링 내부의 `AsyncAnnotationBeanPostProcessor`클래스에 의해 처리됩니다. 이 클래스는 `@Async`가 붙은 메서드를 스캔하고 프록시로 래핑하여 호출 시 비동기 처리가 가능하게 됩니다.
비동기 작업 예외 처리
비동기 작업 중 예외가 발생해도 호출한 스레드로 예외가 전달되지 않기 때문에 AsyncUncaughtExceptionHandler를 통해 비동기 예외를 별도로 처리해야 합니다.
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AsyncConfig implements AsyncConfigurer {
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (throwable, method, objects) -> {
System.err.println("비동기 메서드 예외: " + throwable.getMessage());
};
}
}
Tip: 비동기 작업에서 예외를 잡아야 하는 경우 `CompletableFuture.exceptionally`와 같은 메서드를 활용하면 예외 처리 흐름을 좀 더 유연하게 다룰 수 있습니다.
비동기 작업의 반환값 처리
비동기 작업에서 결과를 반환해야 한다면 `CompletableFuture`를 사용할 수 있습니다.
@Async("customThreadPool")
public CompletableFuture<String> asyncMethodWithReturn() {
return CompletableFuture.completedFuture("Result");
}
이렇게 하면 비동기 작업에서 반환값을 받을 수 있으며 `CompletableFuture`의 다양한 메서드를 통해 비동기 결과를 기다리거나 예외를 처리할 수 있습니다.
💡`@Async`는 스프링에서 비동기 작업을 쉽게 처리할 수 있게 해 주는 도구입니다.
이를 활용해 시스템의 성능과 응답성을 크게 향상할 수 있습니다. 하지만 적절한 스레드 풀 설정과 예외 처리, 비동기 메서드 호출 시의 주의점 등을 충분히 고려하여 사용해야 합니다.
'Backend > Spring' 카테고리의 다른 글
| [Spring]고성능 비동기 웹 개발의 시작: Spring WebFlux 알아보기 (0) | 2024.11.11 |
|---|---|
| [Spring] 프록시 패턴(Proxy Pattern) (1) | 2024.11.09 |
| 동시성 문제 해결을 위한 ThreadLocal 이해하기 (1) | 2024.11.02 |
| [Spring] @EventListener VS @TransactionEventListener (0) | 2024.10.26 |
| [Spring] 스프링에서 이벤트 발행과 구독 @EventListener (0) | 2024.10.25 |