[Spring] @Async 로 비동기 처리하기
1. @Async 란?
Spring framework 에서 제공하는 @Async 어노테이션 은 Thread Pool 을 활용하는 비동기 방식의 메서드 실행을 지원한다.
애플리케이션 코어 로직을 수정하지 않고도 * AOP를 통해 메소드를 비동기 처리로 전환할 수 있다.
메서드 호출자는 해당 메서드가 완료될 때까지 기다리지 않고 다음 작업을 진행할 수 있다.
AOP(Aspect-Oriented Programming)는 핵심 로직과 부가 기능을 분리하여 애플리케이션 전체에 걸쳐 사용되는
부가 기능을 모듈화하여 재사용
할 수 있도록 지원한다.
Aspect-Oriented Programming이란 단어를 번역하면 관점(관심) 지향 프로그래밍
이다. 프로젝트 구조를 바라보는 관점을 바꿔보자는 의미이다.
2. ExecutorService 비동기 방식과의 차이
2-1. ExecutorService 란?
Java에서 제공하는 ExecutorService 는 Thread Pool 을 관리해주는 유틸리티 클래스이다.
ExecutorService를 사용하여 여러 개의 메서드를 병렬 처리할 수 있다.
2-2. ExecutorService 를 사용한 비동기 방식 구현
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Main {
// 3개의 쓰레드를 가지는 스레드 풀 생성
ExecutorService executorService = Executors.newFixedThreadPool(3);
//Runnable : return 값이 없는 쓰레드
static Runnable task = new Runnable() {
public void run() {
System.out.println("Thread: " + Thread.currentThread().getName());
}
};
public static void main(String[] args) {
// submit() 은 Callable 또는 Runnable 작업을 쓰레드에 할당
Future<Void> future = executorService.submit(task);
try {
// 작업 처리가 완료되면 null을 리턴
future.get();
} catch (Exception e) {
// Exception Handling
}
// ExecutorService를 더 이상 사용하지 않으면 반드시 종료
executorService.shutdown();
}
}
2-3. ExecutorService 대신 @Async 를 사용하는 이유
간편한 사용
- ExecutorService을 사용하면 비동기 방식의 메소드를 정의 할 때마다, 위와 같이 Runnable의 run()을 재구현해야 하는 등 동일한 작업들을 반복해야 한다.
- @Async 어노테이션을 사용하면 별도의 ExecutorService를 생성하거나 스레드를 직접 관리할 필요 없이 간편하게 비동기 작업을 수행할 수 있다.
CompletableFuture 사용
- @Async 어노테이션은 기본적으로 CompletableFuture를 사용하여 비동기 작업의 결과를 다루기 때문에 Future 객체를 직접 다루지 않아도 된다.
- @Async 어노테이션은 기본적으로 CompletableFuture를 사용하여 비동기 작업의 결과를 다루기 때문에 Future 객체를 직접 다루지 않아도 된다.
일반적으로
간단한 비동기 처리
라면@Async 어노테이션
을 사용하고,커스텀 스레드 풀, 복잡한 비동기 처리
가 필요한 경우에는ExecutorService
를 사용하는 것이 좋다.
여기선 간단한 비동기 처리를 위해 @Async 어노테이션을 사용한다.
3. @Async 를 사용한 비동기 메소드 작성
스프링 프레임워크 5 버전 이전
에서는 만약 커스텀 TaskExecutor 구현체을 따로 생성하지 않았다면, 비동기 작업 처리를 위해 기본적으로 SimpleAsyncTaskExecutor 를 이용해서 스레드를 생성했다.SimpleAsyncTaskExecutor 는
스레드를 재사용하지 않고, 매번 스레드를 새로 만들기 때문에
@Async를 이용하고 싶다면 ThreadPoolTaskExecutor 으로 설정을 변경해야 했다.
스프링 프레임워크 5 버전 이후
부터는 SimpleAsyncTaskExecutor 보다 더 강력한 스레드 풀 기반의 비동기 작업 처리를 위해 ThreadPoolTaskExecutor 가 기본적으로 사용된다. ThreadPoolTaskExecutor는 스레드 풀을 사용하여 작업을 처리하므로
스레드 생성 비용이 줄어들고, 동시에 실행되는 스레드 수를 제한할 수 있다.
4. ThreadPoolTaskExecutor
4-1. 기본 설정
스레드 풀의 설정을 따로 변경하지 않는다면 ThreadPool의 기본 설정은 아래와 같다.
corePoolSize (default : 1)
- thread-pool에 항상 살아있는 thread의 최소 갯수
maxPoolSize (default : Integer.MAX_VALUE)
- thread-pool에서 사용할 수 있는 최대 thread의 갯수
- 작업들이 대기열에 가득 쌓여 corePoolSize 개수의 스레드로 처리할 수 없는 상황이 발생하면, 스레드 풀은 maxPoolSize까지 추가 스레드를 생성하여 작업들을 처리
queueCapacity (default : Integer.MAX_VALUE)
- task가 제출되고 스레드에 의해 수행되기 전 까지 대기하는 queue의 최대 용량
- corePoolSize의 개수를 넘어서는 task가 들어왔을 경우 queue에 해당 task들이 쌓인다.
keepAliveSeconds (default : 60)
- 바쁜 작업시간 동안 추가 생성된 스레드들이 한가해지면 idle 한 상태가 되는데,
corePoolSize를 초과하는 idle 스레드들이 해제되기까지 대기하는 시간
ThreadNamePrefix (default : task-executor-)
- thread의 이름의 prefix
allowCoreThreadTimeOut (default : false)
- 핵심 스레드(core threads)가 일정 시간 동안 작업이 없을 경우에는 타임아웃을 허용하는지 여부를 나타내는 옵션
4-2. ThreadPoolExecutor 사용자 설정
- config 설정을 사용해
Bean으로 등록
- Application class에 @SpringBootApplication이 있고 스레드 풀을 사용하고자 한다면, 스레드 풀 Config 파일에 @EnableAsync 추가
@Configuration @EnableAsync public class AsyncConfig { @Bean(name = "executor") public ThreadPoolTaskExecutor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(3); // 기본 스레드 수 executor.setMaxPoolSize(5); // 최대 스레드 수 executor.setQueueCapacity(10); // 큐 사이즈 executor.setThreadNamePrefix("async-thread-"); executor.initialize(); return executor; } }
4-3. ThreadPoolExecutor 동작 원리
- 기본 스레드의 개수가 전부 사용 중이면 사용 가능한 스레드가 나올 때까지 큐에 대기
- 큐에 작업이 꽉차면 최대 스레드 개수만큼 스레드 추가 생성
- 최대 스레드 사용 중이고 큐에 작업이 꽉차면 RejectedExecutionException 발생
- corePoolSize -> queueCapacity -> MaxPoolSize 순으로 동작
4-4. ThreadPoolExecutor 사용
- 스레드 풀을 사용하고자 하는 위치에서 위에서 등록한 Bean을 주입 받아 사용
참고링크 |
Leave a comment