Spring에서 비동기 프로그래밍을 하기 위해선 ThreadPool
정의가 필요
ThreadPoolExecutor(int corePoolSize, // 최소 유지할 스레드의 개수
int maximumPoolSize, // 최대 할당 가능한 스레드의 개수
long keepAliveTime, // corePoolSize의 개수 보다 많이 할당된 경우 사용되지 않는 경우 keepAliveTime 시간 이후 반환
TimeUnit unit, // keepAliveTime의 시간 단위
BlockingQueue<Runnable> workQueue // 요청을 담아 놓을 Queue
)
CorePoolSize
만큼 Thread를 생성Queue
에 Task를 담는다.Queue
가 다 차게 되면 MaximumPoolSize
만큼 Thread를 생성new ThreadPoolExecutor(5, 10, 3, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(50));
CorePoolSize
값을 너무 크게 생성한 경우 리소스의 낭비가 발생IllegalArgumentException
에러의 발생corePoolSize < 0
, keepAliveTime < 0
, maximumPoolSize <= 0
, maximumPoolSize < corePoolSize
NullPointerException
- workQueue
가 null
AppConfig
클래스에 ThreadPool
에 대한 설정
@Bean(name = "defaultTaskExecutor", destroyMethod = "shutdown")
public ThreadPoolTaskExecutor defaultTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(200);
executor.setMaxPoolSize(300);
return executor;
}
@Bean(name = "messagingTaskExecutor", destroyMethod = "shutdown")
public ThreadPoolTaskExecutor messagingTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(200);
executor.setMaxPoolSize(300);
return executor;
}
AsyncConfig
클래스에 *@EnableAsync
* 선언
@EnableAsync // 비동기 프로그래밍을 하기 위한 Annotation
@Configuration
public class AsyncConfig {
}
EmailService
클래스를 생성하고 @Async
를 사용하는 메서드 생성
@Service
@RequiredArgsConstructor
public class EmailService {
@Async("defaultTaskExecutor")
public void sendMail() {
System.out.println("[sendMail] :: " + Thread.currentThread().getName());
}
@Async("messagingTaskExecutor")
public void sendMailWithCustomThreadPool() {
System.out.println("[sendMailWithCustomThreadPool] :: " + Thread.currentThread().getName());
}
}
Async 활용해 보기
EmailService
를 Bean
주입하여 사용하기
private final EmailService emailService;
public void asyncCall_1() {
System.out.println("[asyncCall_1] ::" + Thread.currentThread().getName()); // 현재 메소드를 실행하는 Thread name 출력
emailService.sendMail();
emailService.sendMailWithCustomThreadPool();
}
[asyncCall_1] ::http-nio-8080-exec-10
[sendMailWithCustomThreadPool] :: messagingTaskExecutor-3
[sendMail] :: defaultTaskExecutor-3
EmailService
를 인스턴스로 생성하여 사용
public void asyncCall_2() {
System.out.println("[asyncCall_2] ::" + Thread.currentThread().getName()); // 현재 메소드를 실행하는 Thread name 출력
EmailService emailService = new EmailService();
emailService.sendMail();
emailService.sendMailWithCustomThreadPool();
}
[asyncCall_2] ::http-nio-8080-exec-4
[sendMail] :: http-nio-8080-exec-4
[sendMailWithCustomThreadPool] :: http-nio-8080-exec-4
AsyncService
안에서 내부 메서드에 @Async
를 사용한 메서드를 사용
public void asyncCall_3() {
System.out.println("[asyncCall_3] ::" + Thread.currentThread().getName()); // 현재 메소드를 실행하는 Thread name 출력
sendMail();
}
@Async
public void sendMail() {
System.out.println("[sendMail] :: " + Thread.currentThread().getName());
}
[asyncCall_3] ::http-nio-8080-exec-6
[sendMail] :: http-nio-8080-exec-6
비동기로 동작하지 않은 원인
메소드를 비동기적으로 동작하기 위해서는 Main Thread에서 Sub Thread로 Task를 넘겨주는 과정이 필요한데 이때 Spring의 힘을 빌리게 된다. 이 과정에서 Spring의 Async Method를 Proxy 객체로 Wrapping하고 이것을 Sub Thread로 넘겨 실행하게 된다.
EmailService
인스턴스를 직접 생성한 경우 Spring으로 부터 이러한 과정에 대한 도움을 받을 수 없기 때문에 비동기로 동작하지 않게 된다.
또한, @Async
한 내부 메서드를 사용할때도 이미 Spring은 AsyncService
라는 Bean
을 가져온 상태고 Caller
와 Async Method
가 하나의 Bean안에서 존재하게 되므로 Spring으로 부터 역시 도움을 받을 수 없다.
앞서 사용한 @Async
메소드들은 Response가 없는 Task들이다. 하지만, 비동기로 처리한 결과값이 필요한 경우가 있을 수 있다. 이 경우 사용할 수 있는 것이 Future
이다.
Future
은 비동기 작업의 결과를 나타내는 객체로 작업이 완료될 때까지 기다리거나 결과를 가져오는 방법을 제공한다. 기본적인 Future
은 기능이 제한적이기 때문에 더 발전된 CompletableFuture
이 주로 사용된다.
TimeoutException
을 던집니다.mayInterruptIfRunning
이 true
이면 실행 중인 작업도 인터럽트할 수 있습니다.Future Interface의 한계점
Future
을 Chaning
하기 어렵다.CompletableFutre
가 사용된다.CompletableFuture.supplyAsync(Supplier<U> supplier)
: 비동기적으로 작업을 실행하고 결과를 반환합니다.CompletableFuture.runAsync(Runnable runnable)
: 비동기적으로 작업을 실행하지만 결과를 반환하지 않습니다.thenApply(Function<? super T,? extends U> fn)
: 작업이 완료된 후 결과를 다른 값으로 변환합니다.thenAccept(Consumer<? super T> action)
: 작업이 완료된 후 결과를 소비합니다.thenRun(Runnable action)
: 작업이 완료된 후 추가 작업을 실행합니다.thenCombine(CompletionStage<? extends U> other, BiFunction<? super T,? super U,? extends V> fn)
: 두 작업의 결과를 결합합니다.thenCompose(Function<? super T,? extends CompletionStage<U>> fn)
: 작업의 결과를 다른 비동기 작업으로 연결합니다.allOf(CompletableFuture<?>... cfs)
: 여러 작업이 모두 완료될 때까지 기다립니다.anyOf(CompletableFuture<?>... cfs)
: 여러 작업 중 하나라도 완료되면 반환합니다.exceptionally(Function<Throwable,? extends T> fn)
: 예외가 발생했을 때 대체 값을 반환합니다.handle(BiFunction<? super T, Throwable, ? extends U> fn)
: 작업이 정상적으로 완료되거나 예외가 발생했을 때 처리합니다.whenComplete(BiConsumer<? super T,? super Throwable> action)
: 작업이 완료되거나 예외가 발생했을 때 추가 작업을 실행합니다.CompletableFuture
사용 예제
CompletableFuture을 사용한 요청을 실행시켜보고 비동기 처리가 이루어지는 지와 @Async
메소드의 결과 확인이 가능한지 확인해 보겠습니다.
Controller
@GetMapping("/future")
public String completableFutureExample() throws ExecutionException, InterruptedException {
Future<String> hello = asyncFutureService.runCompletableFuture();
log.info("do start!");
log.info(asyncFutureService.doDuring());
log.info("do end!");
var result = hello.get();
log.info(result);
return result;
}
Service
@Async("async-thread")
public Future<String> runCompletableFuture() {
log.info("async start running");
var result = CompletableFuture.completedFuture(doLogic());
log.info("async end!");
return result;
}
public String doLogic() {
for (int i = 0; i < 5; i++) {
try {
log.info("before " + (i + 1));
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return "future " + LocalDateTime.now();
}
public String doDuring() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 5; i++) {
sb.append("after ").append(i).append(", ");
}
return sb.toString();
}
결과
[nio-8080-exec-3] c.e.s.controller.AsyncFutureController : do start!
[nio-8080-exec-3] c.e.s.controller.AsyncFutureController : after 0, after 1, after 2, after 3, after 4,
[nio-8080-exec-3] c.e.s.controller.AsyncFutureController : do end!
[ Async-2] c.e.s.service.AsyncFutureService : async start running
[ Async-2] c.e.s.service.AsyncFutureService : before 1
[ Async-2] c.e.s.service.AsyncFutureService : before 2
[ Async-2] c.e.s.service.AsyncFutureService : before 3
[ Async-2] c.e.s.service.AsyncFutureService : before 4
[ Async-2] c.e.s.service.AsyncFutureService : before 5
[ Async-2] c.e.s.service.AsyncFutureService : async end!
[nio-8080-exec-3] c.e.s.controller.AsyncFutureController : future 2024-06-02T01:10:47.886297900
코드를 보았을때 비동기가 아니라면 before
→ after
의 순서로 실행되어야하며 모두 같은 Thread를 사용해야 한다. 그러나, 결과를 보면 비동기 처리가 이루어짐을 확인할 수 있다.
nio-8080-exec-3
에서 실행되었으며 비동기 메서드는 Async-2
에서 실행된 것을 확인할 수 있다.Async Method
가 순서상 먼저이지만 뒤에 있는 after
이 먼저 실행되었으며 Async Method
인 before
은 비동기적으로 실행된 것을 로그를 통해 확인할 수 있다.doLogic()
의 결과값이 반환된 것을 확인할 수 있다.