티스토리 뷰

JVM/Spring

[Spring] @Async 비동기로 처리하는 방법

글을 쓰는 개발자 2022. 10. 1. 18:22
반응형

사용법

초기설정

@SpringBootApplication
@EnableAsync
public class SpringplaygroundApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringplaygroundApplication.class, args);
    }

}

첫 번째로 @EnableAsync을 설정해야 한다. 그래야 비동기로 동작하는 데 필요한 Bean 들이 등록이 된다.

 

돌리고자 하는 클래스 또는 메소드

@Slf4j
@Service
@Async
public class AsyncService {
    public void myAsyncCall() throws InterruptedException {
        Thread.sleep(1000L);
        log.info("async call = {}", LocalDateTime.now());
    }
}

필자는 클래스에 설정하긴 했지만 메소드로 설정해도 상관이 없다.

 

@Slf4j
@Service
@RequiredArgsConstructor
public class MyAsyncService {

    private final AsyncService asyncService;
    
    public void call() throws InterruptedException {
        log.info("start time = {}", LocalDateTime.now());
        asyncCall();
        log.info("end time = {}", LocalDateTime.now());
    }

    public void asyncCall() throws InterruptedException {
        asyncService.myAsyncCall();
    }
}

실제 해당 Async 클래스를 사용하는 서비스를 호출했을 때 다음과 같은 로그가 남는다.

2022-10-03 19:28:10.392  INFO 16905 start time = 2022-10-03T19:28:10.392891
2022-10-03 19:28:10.395  INFO 16905 end time = 2022-10-03T19:28:10.395125
2022-10-03 19:28:11.400  INFO 16905 async call = 2022-10-03T19:28:11.400748

 

서비스에 따라 ThreadPoolTaskExecutor 설정을 다르게 하고 싶을 수 있다.

그랬을 때는 다음과 같이 구성을 해주면 된다.

 

@Configuration
public class ThreadConfiguration {

    @Bean(name = "first")
    public ThreadPoolTaskExecutor asyncFirstExecutor() {
        final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(4);
        executor.setThreadNamePrefix("first");
        executor.initialize();
        return executor;
    }

    @Bean(name = "second")
    public ThreadPoolTaskExecutor asyncSecondExecutor() {
        final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(4);
        executor.setThreadNamePrefix("second");
        executor.initialize();
        return executor;
    }
}

ThreadPoolTaskExecutor 설정 값은 이와 같이 구성할 수 있으며 추가적인 API는 검색을 통해 찾아보시면 될 것 같다.

우선 해당 Bean이름을 편의를 위해 이름을 설정했다.

그리고 해당 이름을 @Async("first") 이와 같이 구성해주면 그에 대한 설정을 쓸 수가 있다.

@Slf4j
@Service
@Async("first")
public class AsyncService {
    public void myAsyncCall() throws InterruptedException {
        Thread.sleep(1000L);
        log.info("async call = {}", LocalDateTime.now());
    }
}
2022-10-03 19:28:31.543  INFO 16905 --- [nio-8080-exec-4] c.p.s.async.MyAsyncService               : start time = 2022-10-03T19:28:31.543265
2022-10-03 19:28:31.543  INFO 16905 --- [nio-8080-exec-4] c.p.s.async.MyAsyncService               : end time = 2022-10-03T19:28:31.543708
2022-10-03 19:28:32.551  INFO 16905 --- [        second2] c.p.s.async.SecondAsyncService           : async call2 = 2022-10-03T19:28:32.551425
2022-10-03 19:28:32.551  INFO 16905 --- [         first2] c.p.springplayground.async.AsyncService  : async call = 2022-10-03T19:28:32.551425

그랬을 때 스레드 이름이 second 와 first 각각 다르게 찍히는 것을 볼 수가 있다.

 

돌아가는 과정

@EnableAsync

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({AsyncConfigurationSelector.class})
public @interface EnableAsync {
    Class<? extends Annotation> annotation() default Annotation.class;

    boolean proxyTargetClass() default false;

    AdviceMode mode() default AdviceMode.PROXY;

    int order() default Integer.MAX_VALUE;
}

여기서 @Import를 통해 AsyncConfigurationSelector.class 빈 정보들을 가져온다.

 

AsyncConfigurationSelector

public class AsyncConfigurationSelector extends AdviceModeImportSelector<EnableAsync> {
    private static final String ASYNC_EXECUTION_ASPECT_CONFIGURATION_CLASS_NAME = "org.springframework.scheduling.aspectj.AspectJAsyncConfiguration";

    public AsyncConfigurationSelector() {
    }

    @Nullable
    public String[] selectImports(AdviceMode adviceMode) {
        switch (adviceMode) {
            case PROXY:
                return new String[]{ProxyAsyncConfiguration.class.getName()};
            case ASPECTJ:
                return new String[]{"org.springframework.scheduling.aspectj.AspectJAsyncConfiguration"};
            default:
                return null;
        }
    }
}

 

여기서 ProxyAsyncConfiguration.class 정보를 보도록 하자

ProxyAsyncConfiguration

@Configuration(
    proxyBeanMethods = false
)
@Role(2)
public class ProxyAsyncConfiguration extends AbstractAsyncConfiguration {
    public ProxyAsyncConfiguration() {
    }

    @Bean(
        name = {"org.springframework.context.annotation.internalAsyncAnnotationProcessor"}
    )
    @Role(2)
    public AsyncAnnotationBeanPostProcessor asyncAdvisor() {
        Assert.notNull(this.enableAsync, "@EnableAsync annotation metadata was not injected");
        AsyncAnnotationBeanPostProcessor bpp = new AsyncAnnotationBeanPostProcessor();
        bpp.configure(this.executor, this.exceptionHandler);
        Class<? extends Annotation> customAsyncAnnotation = this.enableAsync.getClass("annotation");
        if (customAsyncAnnotation != AnnotationUtils.getDefaultValue(EnableAsync.class, "annotation")) {
            bpp.setAsyncAnnotationType(customAsyncAnnotation);
        }

        bpp.setProxyTargetClass(this.enableAsync.getBoolean("proxyTargetClass"));
        bpp.setOrder((Integer)this.enableAsync.getNumber("order"));
        return bpp;
    }
}

해당 클래스를 통해 @EnableAsync 설정한 정보들을 이용한다.

 

AsyncAnnotationBeanPostProcessor

위 ProxyAsyncConfiguration에서 볼 수 있다시피 AsyncAnnotationBeanPostProcessor를 통해  Advice로 동작하게 된다.

 

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

interceptor를 등록하는 과정이며 자세한 것은 검색해보시길 바란다.

 

AsyncExecutionAspectSupport

protected Object doSubmit(Callable<Object> task, AsyncTaskExecutor executor, Class<?> returnType) {
   if (CompletableFuture.class.isAssignableFrom(returnType)) {
      return executor.submitCompletable(task);
   }
   else if (org.springframework.util.concurrent.ListenableFuture.class.isAssignableFrom(returnType)) {
      return ((org.springframework.core.task.AsyncListenableTaskExecutor) executor).submitListenable(task);
   }
   else if (Future.class.isAssignableFrom(returnType)) {
      return executor.submit(task);
   }
   else if (void.class == returnType) {
      executor.submit(task);
      return null;
   }
   else {
      throw new IllegalArgumentException(
            "Invalid return type for async method (only Future and void supported): " + returnType);
   }
}

을 통해 실제 작업을 처리하게 된다.

 

언제 사용하면 좋을까?

주로 문자 메세지, 이메일 등을 처리하기 위해 많이 사용될 것이다.

그 뿐만 아니라 클라이언트에게 빠른 결과를 보여주고 해당 처리는 비동기로 처리함으로써 유저들에게 더 좋은 서비스를 경험해줄 때 쓰일 수 있다.

 

반응형

'JVM > Spring' 카테고리의 다른 글

[Java] record 이해해보기  (0) 2022.10.06
[Spring] @EventListener  (0) 2022.10.06
[Spring] @Scheduled, @EnableScheduling  (0) 2022.10.01
[Spring] Scope (Prototype & Singleton)  (2) 2022.09.13
[Hikari CP] 光 살펴보기 - 4  (0) 2022.04.29
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
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
글 보관함