스프링 비동기 (Asynchronous)
외부 API에 작업 요청하고 외부 API서버에서 요청을 처리하는데 오랜시간이 걸리는 경우 `비동기 방식`으로 처리하면 효율적이다. 스프링에서 비동기처리를 어떻게하는지 알아보자.
1) @EnableAsync
자바에서는 ExecutorService를 통해서 비동기를 처리할 수 있다. 요청마다 Thread를 찍어내는 방법도 있지만 매 요청 마다 쓰레드가 생성되면 쓰레드 관리가 되지 않아서 위험하다. `ExecutorService`를 사용하면 원하는 크기만큼의 쓰레드 풀을 생성하고 풀에서 쓰레드를 꺼내서 사용하고 다시 반납하는 방식으로 처리한다. 애플리케이션에서 비동기 메서드가 많이 필요한 경우 method를 비동기에 맞게 수정해야하는 번거로움이 있다.
스프링에서는 개발자들의 번거러움을 해결해주기 위해서 `@EnablyAsync`, `@Async` 어노테이션을 제공해준다. `@EnableAsync`, `@Async` 애노테이션 두개로 비동기 메서드를 구현할 수 있다. `@EnableAsync` 애노테이션을 사용하면 `SimpleAsyncTaskExecutor`를 사용하도록 설정되어 있다. `SimpleAsyncTaskExecutor`는 매번 쓰레드르 생성하는 방식이기때문에 설정을 오버라이딩해서 사용하는게 좋다.
ThreadPool생성하는 bean생성하는 방법
설정을 오버라이딩 하는 방법은 ThreadPoolExecutor를 생성하는 `@Bean`을 생성시키고 `@Async` 어노테이션을 메서드에 태깅시킬때 빈 이름을 설정해주는 방법이 있다. 이 방법을 사용하면 여러개의 쓰레드풀을 조건을 여러개 만들고 환경에 따라 다르게 쓰레드풀을 잡을수 있다. (dev,test,production)
package com.example.demo.Asynchronous;
import java.util.concurrent.Executor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@Configuration
@EnableAsync
public class SpringAsyncConfig {
@Bean(name = "threadPoolTaskExecutor")
public Executor threadPoolTaskExecutor()
{
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(3);
taskExecutor.setMaxPoolSize(30);
taskExecutor.setQueueCapacity(10);
taskExecutor.setThreadNamePrefix("Executor-");
taskExecutor.initialize();
return taskExecutor;
}
}
AsyncConfigurerSupport 상속받는 방법
`AsyncConfigurerSupport`를 상속받아서 오버라이딩 시키는 방식으로 설정을 할 수 있다.
@Configuration
@EnableAsync
public class AsyncConfig extends AsyncConfigurerSupport {
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(3);
executor.setMaxPoolSize(30);
executor.setQueueCapacity(10);
executor.setThreadNamePrefix("ThreadPoolTaskExecutor-");
executor.initialize();
return executor;
}
}
2) @Async를 통해서 비동기 메서드 테스트
이제 스프링프레임워크에서 비동기 요청을 설정하는 방법을 알게되었다. 임의로 비동기 로직이 포함되어있는 컨트롤러를 만들고 어떻게 정말로 비동기방식으로 처리되는지 확인해보자. 비교를 위해서 blocking방식으로 핸들러 nonblocking방식으로 동작하는 핸들러를 만들었다. 쿼리스트링으로 처리시간이 들어와서 동기식 방식으로는 처리시간만큼 blocking당하고 비동기 방식은 처리결과와 상관없이 바로 반환하도록 구현했다.
package com.example.demo.Asynchronous;
import java.util.Timer;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.async.DeferredResult;
@Slf4j
@RestController
@AllArgsConstructor
public class AsyncController {
AsyncService asyncService;
@GetMapping("/blocking")
public String blockingProcess(@RequestParam(value="processingTime") long processingTime)
throws InterruptedException {
log.info("blocking process start");
long sTime = System.currentTimeMillis();
Thread.sleep(processingTime);
long eTime = System.currentTimeMillis();
long timeTaken = eTime - sTime;
String result = "SUCCESS";
log.info("blocking process end");
return result + "/blocking completed in" + timeTaken + "Ms";
}
@GetMapping("/nonblocking")
public String nonBlockingProcess(@RequestParam(value="processingTime") long processTime) {
log.info("nonblocking process start");
asyncService.onAsyncTask(processTime);
return "SUCCESS /nonblocking compled in";
}
}
아래는 컨트롤러단에서 처리시간이 파라미터로 들어와서 처리시간동안 쓰레드를 sleep시키는 서비스이다.(외부 API가 요청에 대한 응답시간이 오래걸리는 상황을 가정)
package com.example.demo.Asynchronous;
import java.util.Timer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class AsyncService {
@Async("threadPoolTaskExecutor")
public void onAsyncTask(long processingTime) {
log.info("async task start");
long sTime = System.currentTimeMillis();
long eTime;
try {
Thread.sleep(processingTime);
eTime = System.currentTimeMillis();
} catch (InterruptedException e) {
e.printStackTrace();
eTime = System.currentTimeMillis();
}
log.info("async task end");
}
}
로그를 확인해보면 async task start가 로그에 찍히기전에 전에 클라이언트에 먼저 응답이 간다.
2020-08-16 16:02:56.448 INFO 30497 --- [nio-8080-exec-6] c.e.d.A.AsyncController : nonblocking process start
2020-08-16 16:02:56.449 INFO 30497 --- [ Executor-2] c.e.d.A.AsyncService : async task start
2020-08-16 16:02:59.093 INFO 30497 --- [ Executor-1] c.e.d.A.AsyncService : async task end
3) @Async 원리 (정리중..)
`@Async` 어노테이션을 태깅시켜서 메서드가 비동기로 동작하는것을 확인하였다. 그렇다면 스프링 프레임워크 내부에서 어떻게 처리하길래 `@Async` 어노테이션만 붙이면 비동기처리가 되는걸까? 아래와 같이 메서드위에 `@Async`를 태깅시기면 스프링은 스레드풀을 찾는다. 기본 스레드풀로 `SimpleAsyncTaskExecutor` 풀을 사용하는데 따로 설정을 해주면 설정한 스레드풀을 사용한다. 스레드풀을 찾으면 Job을 스레드풀에 넘겨준다.
@Async
public void asynchronouseMethod() {
//do something
}
스프링 내부코드를 확인해보면 `AOP` 로 비동기처리를 수행하고 있다. AsyncAnnotationBeanPostProcessor라는 객체가 AbstractBeanFactoryAwareAdvisingPostProcessor라는 객체를 상속받고있는 구조이다. (복잡한 구조이므로 차근차근 정리해보자...)
참고
'Spring & Spring Boot' 카테고리의 다른 글
Feign Client (0) | 2020.10.08 |
---|---|
컨트롤러 테스트 @WebMvcTest vs @SpringBootTest (0) | 2020.07.16 |
RequestContextHolder (0) | 2020.06.29 |
스프링 웹 프로그래밍 (2) (0) | 2020.02.22 |
스프링 웹 프로그래밍 (1) (0) | 2020.02.22 |
댓글