본문 바로가기
Back-end

Spring @Async + ThreadPoolTaskExecutor

by 신재권 2024. 1. 14.

동기/비동기

동기

함수의 종료 시점과, 결과를 반환 받은 시점이 같은 경우

 

비동기

함수의 종료 시점과, 결과를 반환 받은 시점이 다른 경우

 

Spring @Async

Spring 에서는 @Async 어노테이션을 통해 비동기 처리 지원

@Async 어노테이션을 적용하면 내부적으로 프록시로 동작

Caller Thread와 다른 Thread에서 비동기 작업 동작

 

내부적으로 proxy를 사용하기 때문에 private 메서드에 적용 불가

또한 self-invocationg 도 불가하다. → proxy 미적용 문제

 

  1. Spring Context에 등록되어 있는 Async Bean 호출
  2. Spring이 Async Bean을 Proxy로 Wrapping
    • Spring Container에 의해 Bean으로 등록되는 시점에 프록시 객체화
  3. Async Bean을 호출한 객체는 AOP를 통해 만들어진 프록시 객체화된 Async Bean을 참조

 

가능한 반환 타입

  • void
  • Future<T>
  • ListenableFuture<T>

 

  • 왜 String 은 사용할 수 없을까?
    • 비동기는 함수의 종료 시점과, 결과를 반환 받은 시점이 다르다.
    • 함수가 종료되었다고 결과를 받았다고 확신할 수 없다.
      • void 를 제외한 반환 타입은 콜백을 정의하여 결과를 반환 받고 어떻게 핸들링 할지 결정할 수 있다.

SimpleAsyncTaskExecutor

@Async가 사용하는 기본 TaskExecutor

  • SimpleAsyncTaskExecutor(Default)
    • TaskExecutor 빈을 따로 선언하지 않으면 기본적으로 스프링이 빈으로 등록
    • 매 작업마다 새로운 스레드를 생성하고 비동기 방식으로 동작
      • 스레드 풀 방식의 Executor가 아님
      • 즉, 스레드를 재사용하지 않음

 

  • 다음 타입의 빈이 하나라도 존재하면
    • Executor
    • ExecutorService
    • TaskExecutor
  • @Async(”taskExecutor”)

 

Spring ThreadPoolTaskExecutor 규칙

  • 빈으로 등록된 TaskExecutor이 하나인 경우 해당 빈 사용
  • 2개 이상인 경우 이름이 taskExecutor인 빈을 사용
  • taskExecutor 빈이 없는 경우 SimpleAsyncTaskExecutor을 사용

 

아래 설정으로 동작

  • core pool size → 너무 작으면 낮은 처리량, 크면 그 만큼의 스레드는 사용되지 않더라도 항상 대기 상태로 리소스 낭비
  • max pool size
  • queue capacity
    • 지정된 QueueCapacity가 0보다 크다면 LinkedBlockingQueue 사용
    • 그렇지 않으면 task를 바로 넘겨주는 SynchronousQueue 사용

 

  1. 사용하고 있는 스레드가 core pool size 만큼 사용하고 있다면?
  2. queue 에서 대기 시킴
  3. queue의 capacity를 초과했다면?
  4. max pool size 만큼 스레드 수 증가

 

예제 코드

/**
 * Async 스레드 풀 설정
 *
 * corePoolSize: 기본 스레드 풀 크기
 * maxPoolSize: 최대 스레드 풀 크기
 * queueCapacity: 최대 큐 크기
 *
 * CallerRunsPolicy : 3번 과정 까지 도달 시 Handling 정의
 * - shutdown 상태가 아닐 때 요청한 Caller Thread 에서 작업 처리
 *
 * setWaitForTasksToCompleteOnShutdown : 서버 종료 후 대기 여부, task 유실 방지
 * setAwaitTerminationSeconds : 서버 종료 후 time out
 *
 * 1. 기존 스레드 갯수 값 까지 스레드 할당(corePoolSize)
 * 2. 큐 크기 만큼 task 큐에서 대기(queueCapacity)
 * 3. 최대 스레드 갯수 만큼 스레드 추가(maxPoolSize)
 *
 * @author 신재권
 * */
@EnableAsync
@Configuration
class AsyncConfig {
    @Bean
    fun taskExecutor() : TaskExecutor{
        return ThreadPoolTaskExecutor().apply {
            this.corePoolSize = 10
            this.maxPoolSize = 20
            this.queueCapacity = 100
            this.threadNamePrefix = "async-exec"
            this.setRejectedExecutionHandler(ThreadPoolExecutor.CallerRunsPolicy())
            this.setWaitForTasksToCompleteOnShutdown(true)
            this.setAwaitTerminationSeconds(30)
            this.initialize()
        }
    }
}

 

비동기 스프링 @MVC

비동기 @MVC 리턴 타입

  • Callable<T>
    • AsyncTaskExecutor에서 실행될 코드를 리턴
  • WebAsyncTask<T>
    • Callable의 timeout과 taskExecutor를 추가
  • DeferredResult<T>
    • 임의의 스레드에서 리턴 값 설정 가능
    • Callable 처럼 새로운 스레드를 만들지 않음
    • 다양한 비동기 처리 기술과 손쉽게 결합
    • @Async 메서드가 리턴하는 ListenableFuture에서 DeferredResult 사용
    • 비동기 @MVC + 비동기 메서드 실행
  • ListenableFuture<T>
    • DeferredResult + 콜백 생략 가능
    • 한계 존재
      • 두 가지 이상의 비동기 작업을 순차적으로 혹은 동시에 수행하고 결과를 조합해서 @MVC의 리턴 값으로 넘기려면?
      • DeferredResult 를 사용하여 콜백을 정의해줘야 한다.
      • 콜백 헬 발생
  • CompletionStage<T>
    • 함수형 스타일 접근 방법
    • CompletionStage의 조합으로 간결하게 표현
    • 중복되는 예외 처리를 한번에
    • 다양한 비동기/동기 작업의 변환, 조합, 결합 가능
      • 2개 이상의 비동기 작업을 병렬적으로 실행하고 결과를 모아서 결과 값을 만들어 내는 비동기 작업 구성
  • ResponseBodyEmitter

 

비동기 논블로킹 API 호출

  • RestTemplate은 동기-블로킹 방식
  • API 호출 작업 동안 스레드 점유
  • 블로킹으로 인한 컨텍스트 스위칭 발생
  • 비동기 @MVC를 사용했다고 하더라도 스레드 자원의 효율적인 사용이 어려움

 

AsyncRestTemplate

  • 스프링 4.0 부터 지원
  • RestTemlpate의 비동기-논블로킹 버전
  • 비동기, 논블로킹(콜백)으로 결과를 돌려 받지만 논블로킹 IO를 사용하지 않음
    • NIO 팩토리 사용(Netty)

 

비동기 작업과 API 호출이 많은 @MVC 앱

  • @MVC
  • AsyncRestTemplate + Non-Blocking IO 라이브러리
  • @Async와 적절한 TaskExecutor
  • ListenableFuture, CompletableFuture

스레드를 얼마나 할당할지 잘 설정하자

 

비동기 스프링 기술을 사용하는 이유

  • IO가 많은 서버 앱에서 서버 자원을 효율적으로 사용해 성능을 높임
  • 서버 외부의 이벤트를 받아 처리하는 것과 같은 비동기 작업 필요

출처

스프링캠프 2017 [Day1 A2] : Async & Spring

'Back-end' 카테고리의 다른 글

Spring AutoConfiguration  (0) 2024.01.20
Spring Validation in Kotlin  (0) 2024.01.14
Spring Security Filter  (0) 2024.01.01
코틀린 - 기본 문법  (1) 2023.10.25
JPA Entity 기본 생성자  (0) 2023.09.29