0%

springboot 비동기 서비스 만들기(Async)

들어가기

springboot rest 서버에서 어떤 요청을 받으면, Shell command를 호출하는 기능을 구현해야 한다.

문제는 Shell command로 호출하는 호출하는 것이 Python 스크립트이고, 이 스크립트 동작이 몇분은 걸린다는 것이다.

대충 구조를 보면 아래와 같다.

springboot%2042b5ceb5f304486093b4108772978651/Untitled.png

지금 고민하고 있는 문제는, spring 컨트롤러가 호출하는(서비스를 통해서) python 스크립트의 동작이 오래 걸린다는 것이다.

즉 다음과 같은 문제가 발생한다.

springboot%2042b5ceb5f304486093b4108772978651/Untitled%201.png

spring 컨트롤러가 호출 한 파이썬의 동작이 끝나지 않아 브라우저가 그 응답을 기다려야 하는 것이다.

그래서 지금 내가 원하는 컨트롤러의 동작은 다음과 같다.

springboot%2042b5ceb5f304486093b4108772978651/Untitled%202.png

구글링을 해보니 spring 컨트롤러가 호출하는 service의 메소드에 Async라는 어노테이션을 적용하면 위와 같은 동작을 구현 할 수 있다고 한다. 한번 간단하게 만들어보자.

@Async 어노테이션을 사용해서 비동기 서비스 만들기

일단 아주 간단한 springboot 웹 프로젝트를 생성했다.

spring에서 Async, 즉 비동기 기능을 사용하는 방법은 아주 간단하다.

  • @EnableAsync로 비동기 기능을 활성화
  • 비동기로 동작을 원하는 메소드(public 메소드)에 @Async 어노테이션을 붙여준다.

간단하게 위 두가지 행위로 spring에서 비동기 기능을 구현 할 수 있다.

springboot에서 Async 비동기 기능 활성화 및 thread pool 설정

AsyncConfig.java 라는 java config 파일을 추가했다.

springboot%2042b5ceb5f304486093b4108772978651/Untitled%203.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.example.demo;

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurerSupport;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@Configuration
@EnableAsync
public class AsyncConfig extends AsyncConfigurerSupport {

@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("hanumoka-async-");
executor.initialize();
return executor;
}
}
  • @EnableAsync : spring의 메소드의 비동기 기능을 활성화 해준다.
  • ThreadPoolTaskExecutor로 비동기로 호출하는 Thread 대한 설정을 한다.
    • corePoolSize: 기본적으로 실행을 대기하고 있는 Thread의 갯수
    • MaxPoolSise: 동시 동작하는, 최대 Thread 갯수
    • QueueCapacity : MaxPoolSize를 초과하는 요청이 Thread 생성 요청시 해당 내용을 Queue에 저장하게 되고, 사용할수 있는 Thread 여유 자리가 발생하면 하나씩 꺼내져서 동작하게 된다.
    • ThreadNamePrefix: spring이 생성하는 쓰레드의 접두사를 지정한다.

이제 비동기로 동작할 메소드를 갖는 Service를 생성해보자.

@Async를 적용한 service 만들기

AsyncService.java 소스코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.example.demo;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
public class AsyncService {

private static final Logger logger = LoggerFactory.getLogger(AsyncService.class);

//비동기로 동작하는 메소드
@Async
public void onAsync() {
try {
Thread.sleep(5000);
logger.info("onAsync");
} catch (InterruptedException e) {
e.printStackTrace();
}
}

//동기로 동작하는 메소드
public void onSync() {
try {
Thread.sleep(5000);
logger.info("onSync");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

springboot%2042b5ceb5f304486093b4108772978651/Untitled%204.png

Service의 비동기로 호출할 메소드 위에 @Async 어노테이션을 달아주면 해당 메소드는 호출 시 비동기로 동작하게 된다. 따라서 해당 메소드를 호출한 Thread(예를 들어 Controller)는 논 블록킹으로 동작하게 된다.

주의 할 점은 private 메소드는 @Async 를 적용해도 비동기로 동작하지 않으며, 반드시 public 메소드에 @Async 를 적용해야 한다.

이제 위 서비스를 호출한 컨트롤러를 만들다.

컨트롤러 만들기

컨트롤러는 일반적인 컨트롤러와 다르지 않다.

AsyncController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.example.demo;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class AsyncController {

Logger logger = LoggerFactory.getLogger(AsyncController.class);

@Autowired
AsyncService asyncService;

@Autowired
private AsyncService service;

@GetMapping("/async")
public String goAsync() {
service.onAsync();
String str = "Hello Spring Boot Async!!";
logger.info(str);
logger.info("==================================");
return str;
}

@GetMapping("/sync")
public String goSync() {
service.onSync();
String str = "Hello Spring Boot Sync!!";
logger.info(str);
logger.info("==================================");
return str;
}

}

비동기 요청을 컨트롤러에 보내보았다.

오른쪽 브라우저에서는 응답이 떨어졌으므로, 컨트롤러가 response 했다는 의미이다.

하지만 아직 스프링 console에서 비동기 메소드의 동작이 끝나지 않아있다.

springboot%2042b5ceb5f304486093b4108772978651/Untitled%205.png

몇초 뒤 아래처럼 비동기 서비스 동작이 끝나는 것을 확인 할 수 있다.

springboot%2042b5ceb5f304486093b4108772978651/Untitled%206.png

Q1.만약 QueueCapacity를 넘치는 비동기 요청이 있는 경우는 어떻게 될까?

아래 Async 설벙에서 QueueCapacity를 극단적으로 2로 줄여 놓았다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.example.demo;

import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurerSupport;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@Configuration
@EnableAsync
public class AsyncConfig extends AsyncConfigurerSupport {

@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(1);
executor.setMaxPoolSize(2);
executor.setQueueCapacity(2);
executor.setThreadNamePrefix("hanumoka-async-");
executor.initialize();
return executor;
}
}

springboot%2042b5ceb5f304486093b4108772978651/Untitled%207.png

테스트 해 보니, 위처럼 RejectedExecutionException 가 발생한다.

예외내용

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
2020-07-02 12:34:15.419 ERROR 9588 --- [nio-8080-exec-6] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.core.task.TaskRejectedException: Executor [java.util.concurrent.ThreadPoolExecutor@918a620[Running, pool size = 2, active threads = 2, queued tasks = 2, completed tasks = 0]] did not accept task: org.springframework.aop.interceptor.AsyncExecutionInterceptor$$Lambda$483/564857467@35c2afb4] with root cause

java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@7df58ab rejected from java.util.concurrent.ThreadPoolExecutor@918a620[Running, pool size = 2, active threads = 2, queued tasks = 2, completed tasks = 0]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063) ~[na:1.8.0_232]
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830) [na:1.8.0_232]
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379) [na:1.8.0_232]
at java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:134) ~[na:1.8.0_232]
at org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor.submit(ThreadPoolTaskExecutor.java:341) ~[spring-context-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.aop.interceptor.AsyncExecutionAspectSupport.doSubmit(AsyncExecutionAspectSupport.java:290) ~[spring-aop-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.aop.interceptor.AsyncExecutionInterceptor.invoke(AsyncExecutionInterceptor.java:129) ~[spring-aop-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749) ~[spring-aop-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:691) ~[spring-aop-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at com.example.demo.AsyncService$$EnhancerBySpringCGLIB$$1dc8b7ac.onAsync(<generated>) ~[classes/:na]
at com.example.demo.AsyncController.goAsync(AsyncController.java:23) ~[classes/:na]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_232]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_232]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_232]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_232]
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190) ~[spring-web-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138) ~[spring-web-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105) ~[spring-webmvc-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:879) ~[spring-webmvc-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:793) ~[spring-webmvc-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1040) ~[spring-webmvc-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943) ~[spring-webmvc-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006) ~[spring-webmvc-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:898) ~[spring-webmvc-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:634) ~[tomcat-embed-core-9.0.36.jar:9.0.36]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883) ~[spring-webmvc-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at javax.servlet.http.HttpServlet.service(HttpServlet.java:741) ~[tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231) ~[tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-embed-websocket-9.0.36.jar:9.0.36]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.36.jar:9.0.36]
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.36.jar:9.0.36]
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.36.jar:9.0.36]
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119) ~[spring-web-5.2.7.RELEASE.jar:5.2.7.RELEASE]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202) ~[tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96) [tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541) [tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139) [tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) [tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) [tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343) [tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:373) [tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) [tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868) [tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1590) [tomcat-embed-core-9.0.36.jar:9.0.36]
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-9.0.36.jar:9.0.36]
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [na:1.8.0_232]
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [na:1.8.0_232]
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-9.0.36.jar:9.0.36]
at java.lang.Thread.run(Thread.java:748) [na:1.8.0_232]

따라서 QueueCapacity를 넘치는 비동기 메소드 호출이 시도 될 때 방어하는 코드가 필요해 보인다.

예제소스 github 경로

https://github.com/blog-examples/springboot-async/tree/master

참고자료

https://heowc.dev/2018/02/10/spring-boot-async/

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/scheduling/annotation/EnableAsync.html (EnableAsync docs)

https://jeong-pro.tistory.com/187 (spring async 주의사항)

http://blog.naver.com/PostView.nhn?blogId=goddes4&logNo=30125814709&parentCategoryNo=&categoryNo=&viewDate=&isShowPopularPosts=false&from=postView (ThreadPoolTaskExecutor 설명)