스프링부트 해부학 : Async - @EnableAsync

정윤성·2022년 6월 10일
0

스프링부트 해부학

목록 보기
12/20

역할


출처 : https://jieun0113.tistory.com/73

Request를 보내고 Task를 처리하는도중 파일I/O같은 무거운 작업을 만나게되면 이를 처리하느라 뒤에 모든 할일이 지연이됩니다 즉 그만큼 Response도 늦게되는거죠 ( Sync )

만약 Thread를 추가로 생성해 파일I/O같은 무거운작업은 Worker Thread에서 진행하고 나머지 작업은 빠르게 처리한뒤 Response를 먼저 보내버리게되면 일은 처리가 끝나지 않았어도 Response를 보낼 수 있게되는거죠 ( ASync )

@EnableAsync

@Import(AsyncConfigurationSelector.class)
public @interface EnableAsync {

	Class<? extends Annotation> annotation() default Annotation.class;
    
    AdviceMode mode() default AdviceMode.PROXY;
    
    boolean proxyTargetClass() default false;
}

Default는 Proxy모드로 이는 Spring AOP에서 지원하는 방식이기에 Local에서 접근할때에는 Async가 적용되지 않는다 만약 ASPECTJ로 Mode를 바꾸면 이거는 weaving사용해 직접 컴파일이나 클래스시점에 바이트코드를 넣기때문에 보다 다양하게 AOP를 적용시키고 싶으면 ASPECTJ로 바꾸면 된다

proxyTargetClass는 Proxy Mode일때만 사용가능하며 true일시 interface(리플렉션), false일시 CGLIB방식의 AOP를 적용한다

annotation()을 통해 어떤 @Annotation을 사용할지도 지정할 수 있다

  • default : @Asnyc, @Asynchronous

@Import(AsyncConfigurationSelector.class)를 통해 위에서 적용한 Mode에 따라 다르게 Import해온다

public String[] selectImports(AdviceMode adviceMode) {
	switch (adviceMode) {
    	case PROXY:
        	return new String[] {ProxyAsyncConfiguration.class.getName()};
		case ASPECTJ:
			return new String[] {ASYNC_EXECUTION_ASPECT_CONFIGURATION_CLASS_NAME};
    }
}

ASPECTJ : org.springframework.scheduling.aspectj.AspectJAsyncConfiguration 해당 클래스를 기준으로 Import

PROXY : ProxyAsyncConfiguration 기준으로 Import를 해온다

PROXY Mode에대해서만 좀더 알아보자

ProxyAsyncConfiguration.class


@Bean(name = TaskManagementConfigUtils.ASYNC_ANNOTATION_PROCESSOR_BEAN_NAME)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)public AsyncAnnotationBeanPostProcessor asyncAdvisor() {
	AsyncAnnotationBeanPostProcessor bpp = new AsyncAnnotationBeanPostProcessor();
    ...
    if (customAsyncAnnotation != AnnotationUtils.getDefaultValue(EnableAsync.class, "annotation")) {
    	bpp.setAsyncAnnotationType(customAsyncAnnotation);
    }
    bpp.setProxyTargetClass(this.enableAsync.getBoolean("proxyTargetClass"));
    ...
    return bpp;
}

위에서 설정한 Annotation Value대로 설정하는걸 볼 수 있다

이 중 AsyncAnnotationBeanPostProcessor가 실제 비동기 AOP를 적용시키는 Bean임을 알 수 있는데 이에대해서 자세히 알아보자

AsyncAnnotationBeanPostProcessor

AsyncAnnotationBeanPostProcesor.class

@Override
public void setBeanFactory(BeanFactory beanFactory) {
	...
	AsyncAnnotationAdvisor advisor = new AsyncAnnotationAdvisor(this.executor, this.exceptionHandler);
    if (this.asyncAnnotationType != null) {
    	advisor.setAsyncAnnotationType(this.asyncAnnotationType);
    }
	advisor.setBeanFactory(beanFactory);
	this.advisor = advisor;
}

AsyncAnnotationAdvisor.class

public AsyncAnnotationAdvisor(
	@Nullable Supplier<Executor> executor, @Nullable Supplier<AsyncUncaughtExceptionHandler> exceptionHandler) {
    ...
    asyncAnnotationTypes.add(Async.class);
    ...
    asyncAnnotationTypes.add((Class<? extends Annotation>)
					ClassUtils.forName("javax.ejb.Asynchronous", AsyncAnnotationAdvisor.class.getClassLoader()));
    this.advice = buildAdvice(executor, exceptionHandler);
    this.pointcut = buildPointcut(asyncAnnotationTypes);
}

protected Advice buildAdvice(
	@Nullable Supplier<Executor> executor, @Nullable Supplier<AsyncUncaughtExceptionHandler> exceptionHandler) {
 	AnnotationAsyncExecutionInterceptor interceptor = new AnnotationAsyncExecutionInterceptor(null);
    interceptor.configure(executor, exceptionHandler);
    return interceptor;           
}

위의 setBeanFactory를 통해 실제 Adivosr(PointCut, Advice)와 @Annotation이 등록되는걸 볼 수 있다

여담 : 기본 Task의 이름은 DEFAULT_TASK_EXECUTOR_BEAN_NAME = "taskExecutor"이다

AnnotationAsyncExecutionInterceptor은 이따 밑에서 알아보자

AbstractAdvisingBeanPostProcessor.java

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
	if (bean instanceof Advised) {
    	Advised advised = (Advised) bean;
        if (!advised.isFrozen() && isEligible(AopUtils.getTargetClass(bean))) {
        ...
        advised.addAdvisor(this.advisor);
        return bean;
    }
    
    if (isEligible(bean, beanName)) {
    	ProxyFactory proxyFactory = prepareProxyFactory(bean, beanName);
     	proxyFactory.addAdvisor(this.advisor);
        ...
        return proxyFactory.getProxy(classLoader);
    }
    return bean;
}

BestAfterProcessor에서 ProxyFactory를 통해 Advice가 등록되는걸 볼 수 있다

AnnotationAsyncExecutionInterceptor.class

@Override
public Object invoke(final MethodInvocation invocation) throws Throwable {
	...
    AsyncTaskExecutor executor = determineAsyncExecutor(userDeclaredMethod);
    ...
    Callable<Object> task = () -> {
    	Object result = invocation.proceed();
        if (result instanceof Future) {
        	return ((Future<?>) result).get();
        }
        ...
    }
    ...
    return doSubmit(task, executor, invocation.getMethod().getReturnType());
}

AnnotationAsyncExecutionInterceptor는 위에서 등록된 Advice로 Proxy객체에 접근할때 해당 Advice가 실행되는걸 알 수 있다

invoke메서드는 determineAsyncExectuor에 의해 Async전략을 정한뒤 해당 Thread를 실행시켜 Callback메서드를 받아온다

기본적으로 Async는 Bean이 등록된 상태로 Advice를 진행하는 것 이다

Pointcut cpc = new AnnotationMatchingPointcut(asyncAnnotationType, true);
Pointcut mpc = new AnnotationMatchingPointcut(null, asyncAnnotationType, true);

다음과같이 클래스대상 / 메서드대상으로 PointCut을 한다

AsyncTaskExecutor.class

SimpleAsyncTaskExecutor.class

protected void doExecute(Runnable task) {
	Thread thread = (this.threadFactory != null ? this.threadFactory.newThread(task) : createThread(task));
    thread.start();
}

계속 만들어서 사용

ThreadPoolTaskExecutor.class

public void execute(Runnable task) {
	Executor executor = getThreadPoolExecutor();
	try {
		executor.execute(task);
	}
}

ThreadPool사용

ConcurrentTaskExecutor.class

@Override
public Future<?> submit(Runnable task) {
	return super.submit(ManagedTaskBuilder.buildManagedTask(task, task.toString()));
}

@Override
public ListenableFuture<?> submitListenable(Runnable task) {
	return super.submitListenable(ManagedTaskBuilder.buildManagedTask(task, task.toString()));
}

Future사용

이외의 더 많은 TaskExecutor가 존재하며 Scheduler또한 위 인터페이스를 구현한다

정리

  1. Async는 결국 BeanProcessor에 의해 Proxy객체로 반환되고 이를 사용시 Interceptor에 의해 Executor가 실행이 된다 ( 포인트컷은 @Async가 달린 클래스나 메서드 )
  2. 위의 과정에 의해 Bean이 등록이 되어있어야 Async가 작동한다
  3. 다양한 TaskExecutor전략에 대해 알 수 있었다

Quiz

@Configuration
@RequestMapping("/test")
public class TestController2 {
	
    @ResponseBody
    public String testBean() throws InterruptedException {
        this.runAsync();
        return "Clear";
    }

	@Bean
    @Async
    public TestBean2 runAsync() throws InterruptedException {
        this.logger.info("After Good Async2");
        return new TestBean2("Time : " + LocalDateTime.now());;
    }
}

Bean만 등록되어있으면 비동기는 실행이 되는것 인데 해당 Async는 정상적으로 출력이 될까 ?

Answer : 실제 비동기로 돌아가나 해당 FactoryMethod Bean이 실행되고나서 return Object받는 과정이 동기적이지 않아 이부분이 빠르게 패스되어 Bean으로 등록되어지지 않는다. 때문에 @Configuration이 Component이므로 Bean으로 등록되어 @Async가 포인트컷으로 등록이 되는데 이 때 @Configuration이 해당 메서드를 호출하는 과정에서 Proxy가 Null을 전달하므로 정상적으로 처리되지 않는다
profile
게으른 개발자

1개의 댓글

comment-user-thumbnail
2024년 6월 11일

proxyTargetClass는 Proxy Mode일때만 사용가능하며 true일시 interface(리플렉션), false일시 CGLIB방식의 AOP를 적용한다

설명이 반대로 되어있습니다! 공식 문서에는 다음과 같이 되어있어요

Indicate whether subclass-based (CGLIB) proxies are to be created as opposed to standard Java interface-based proxies.

답글 달기