동기/비동기
동기
함수의 종료 시점과, 결과를 반환 받은 시점이 같은 경우
비동기
함수의 종료 시점과, 결과를 반환 받은 시점이 다른 경우
Spring @Async
Spring 에서는 @Async 어노테이션을 통해 비동기 처리 지원
@Async 어노테이션을 적용하면 내부적으로 프록시로 동작
Caller Thread와 다른 Thread에서 비동기 작업 동작
내부적으로 proxy를 사용하기 때문에 private 메서드에 적용 불가
또한 self-invocationg 도 불가하다. → proxy 미적용 문제
- Spring Context에 등록되어 있는 Async Bean 호출
- Spring이 Async Bean을 Proxy로 Wrapping
- Spring Container에 의해 Bean으로 등록되는 시점에 프록시 객체화
- 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 사용
- 사용하고 있는 스레드가 core pool size 만큼 사용하고 있다면?
- queue 에서 대기 시킴
- queue의 capacity를 초과했다면?
- 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가 많은 서버 앱에서 서버 자원을 효율적으로 사용해 성능을 높임
- 서버 외부의 이벤트를 받아 처리하는 것과 같은 비동기 작업 필요
출처
'Back-end' 카테고리의 다른 글
Spring AutoConfiguration (0) | 2024.01.20 |
---|---|
Spring Validation in Kotlin (0) | 2024.01.14 |
Spring Security Filter (0) | 2024.01.01 |
코틀린 - 기본 문법 (0) | 2023.10.25 |
JPA Entity 기본 생성자 (0) | 2023.09.29 |