[Spring] @Async Annotation(비동기 메소드 사용하기)

GilLog·2021년 8월 27일
22

Spring

목록 보기
20/22
post-custom-banner

🙆‍♂️ import 🙇‍♂️

springboot 비동기 서비스 만들기(Async)-Hanumoka, IT Blog

How does @Async work? @Async를 지금까지 잘 못 쓰고 있었습니다(@Async 사용할 때 주의해야 할 것, 사용법)[기본기를 쌓는 정아마추어 코딩블로그]

Effective Advice on Spring Async: Part 1[DZone]


@Async

@Async AnnotationSpring에서 제공하는 Thread Pool을 활용하는 비동기 메소드 지원 Annotation이다.

기존 Java에서 비동기 방식으로 메소드를 구현할 때는 아래와 같이 구현할 수 있었다.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class GillogAsync {

    static ExecutorService executorService = Executors.newFixedThreadPool(5);

    public void asyncMethod(final String message) throws Exception {
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                // do something
            }            
        });
    }
}

java.util.concurrent.ExecutorService을 활용해서 비동기 방식의 method를 정의 할 때마다,

위와 같이 Runnablerun()을 재구현해야 하는 등 동일한 작업들의 반복이 잦았다.

With @Async

@Async Annotation을 활용하면 손쉽게 비동기 메소드 작성이 가능하다.

만약 Spring Boot에서 간단히 사용하고 싶다면, 단순히 Application Class에 @EnableAsync Annotation을 추가하고,

@EnableAsync
@SpringBootApplication
public class SpringBootApplication {
    ...
}

비동기로 작동하길 원하는 method 위에 @Async Annotation을 붙여주면 사용할 수 있다.

public class GillogAsync {

    @Async
    public void asyncMethod(final String message) throws Exception {
        ....
    }
}

위와 같은 사용은 간단하지만 @Async의 기본설정인 SimpleAsyncTaskExecutor를 사용한다.

본인의 개발 환경에 맞게 Customize하기에는 직접 AsyncConfigurerSupport를 상속받는 Class를 작성하는 것이 좋다.

AsyncConfigurerSupport

아래와 같은 AsyncConfigurerSupport를 상속받는 Customize Class를 구현하자.

import java.util.concurrent.Executor;

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

@Configuration
@EnableAsync
public class AsyncConfig extends AsyncConfigurerSupport {
    
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(30);
        executor.setQueueCapacity(50);
        executor.setThreadNamePrefix("DDAJA-ASYNC-");
        executor.initialize();
        return executor;
    }
}

여기서 설정한 요소들은 아래와 같다.

  • @Configuration : Spring 설정 관련 Class로 @Component 등록되어 Scanning 될 수 있다.
  • @EnableAsync : Spring method에서 비동기 기능을 사용가능하게 활성화 한다.
  • CorePoolSize : 기본 실행 대기하는 Thread의 수**
  • MaxPoolSize : 동시 동작하는 최대 Thread의 수
  • QueueCapacity : MaxPoolSize 초과 요청에서 Thread 생성 요청시,
    해당 요청을 Queue에 저장하는데 이때 최대 수용 가능한 Queue의 수,
    Queue에 저장되어있다가 Thread에 자리가 생기면 하나씩 빠져나가 동작
  • ThreadNamePrefix : 생성되는 Thread 접두사 지정

위와 같이 작성한 후 비동기 방식 사용을 원하는 method에 @Async Annotation을 지정해주면 된다.


주의사항

@Async Annotation을 사용할 때 아래와 같은 세 가지 사항을 주의하자.

  1. private method는 사용 불가

  2. self-invocation(자가 호출) 불가, 즉 inner method는 사용 불가

  3. QueueCapacity 초과 요청에 대한 비동기 method 호출시 방어 코드 작성

위 주의사항을 아래 사진과 함께 설명을 해보면,

출처 : https://dzone.com/articles/effective-advice-on-spring-async-part-1

@Async의 동작AOP가 적용되어 Spring Context에서 등록된 Bean Object의 method가 호출 될 시에,

Spring이 확인할 수 있고 @Async가 적용된 method의 경우 Spring이 method를 가로채 다른 Thread에서 실행 시켜주는 동작 방식이다.

이 때문에 Spring이 해당 @Async method를 가로챈 후, 다른 Class에서 호출이 가능해야 하므로,

private method는 사용할 수 없는 것이다.


또한 Spring Context에 등록된 Bean의 method의 호출이어야 Proxy 적용이 가능하므로,

inner method의 호출은 Proxy 영향을 받지 않기에 self-invocation이 불가능하다.

위 주의사항을 아래 예시 코드와 함께 살펴보자

self-invocation(자가 호출) 불가

위에서 작성한 AsyncConfig를 사용하는 Spring Project에서 아래와 같이,

같은 Class에 존재하는 method에 @Async Annotation을 작성해 비동기 방식을 사용해보자.

@Controller
public Class TestController {

    @Async
    public void asyncMethod(int i) {
        try {
            Thread.sleep(500);
            log.info("[AsyncMethod]"+"-"+i);
        } catch(InterruptedException e) {
            e.printStackTrace();
        }
    }

    @GetMapping("async")
    public String testAsync() {
        log.info("TEST ASYNC");
        for(int i=0; i<50; i++) {
            asyncMethod(i);
        }
        return "";
    }
}

작동 결과를 보면 비동기 방식으로 호출되지 않았고, 동기적으로 호출 순서대로 동작하는 것을 확인할 수 있다.

자가 호출에서는 @Async 사용이 불가하다.


하지만, @Service로 Bean 등록된 Service를 통해 주입하여 위 코드를 다시 작성해보면,

@Service
public class TestService {
    @Async
    public void asyncMethod(int i) {
        try {
            Thread.sleep(500);
            log.info("[AsyncMethod]"+"-"+i);
        } catch(InterruptedException e) {
            e.printStackTrace();
        }
    }
}

@AllArgsConstructor
@Controller
public Class TestController {

    private TestService testService;

    @GetMapping("async")
    public String testAsync() {
        log.info("TEST ASYNC");
        for(int i=0; i<50; i++) {
            testService.asyncMethod(i);
        }
        return "";
    }
    
}

위 사진과 같이 호출 순서에 상관없이 비동기 방식으로 method가 호출 되었고,

AsyncConfig에서 prefix로 작성한 접두사도 정상적으로 붙은 것을 확인할 수 있다.

QueueCapacity 초과 요청 방어 코드 작성

이번엔 AsyncConfig에서 PoolSizeQueueCapacity를 줄여보고 위 코드를 다시 실행해보자.

@Configuration
@EnableAsync
public class AsyncConfig extends AsyncConfigurerSupport {
    
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(10);
        executor.setThreadNamePrefix("DDAJA-ASYNC-");
        executor.initialize();
        return executor;
    }
}

Exception이 Throw 되었다.

Exception에서 주요한 사항을 살펴보면

Request processing failed; nested exception is org.springframework.core.task.TaskRejectedException: 
Executor [java.util.concurrent.ThreadPoolExecutor@116e53a0
[Running, pool size = 10, active threads = 10, queued tasks = 10, completed tasks = 0]] 
did not accept task: org.springframework.aop.interceptor.AsyncExecutionInterceptor$$
Lambda$2031/170931344@7ef0a05e] with root cause

TaskRejectedException으로 수행된 task는 0으로 설정 thread 수, pool size 는 10,

queued 된 tasks = 10개로 최대 수용 가능한 Thread Pool 수와 QueueCapacity 까지 초과된 요청이 들어오자,

Task를 Reject하는 Exception이 발생하였다.

아래와 같이 TaskRejectedException 발생 시 handling 해주는 방어 코드를 작성하자.

@AllArgsConstructor
@Controller
public Class TestController {

    private TestService testService;

    @GetMapping("async")
    public String testAsync() {
        log.info("TEST ASYNC");
        try {
            for(int i=0; i<50; i++) {
                testService.asyncMethod(i);
        } catch (TaskRejectedException e) {
            // ....
        }
        return "";
    }
    
}
profile
🚀 기록보단 길록을 20.10 ~ 22.02 ⭐ Move To : https://gil-log.github.io/
post-custom-banner

3개의 댓글

comment-user-thumbnail
2022년 12월 11일

private method는 사용 불가, public method만 사용 가능
-> 이부분, protected 접근자로도 사용 가능하지 않나요?

1개의 답글
comment-user-thumbnail
2024년 4월 7일

자가 호출에서는 @Async 사용이 불가하다.

  • @Async 뿐만 아니라 모든 어노테이션이 작동이 안되지 않을까요?
  • 프록시가 최초 함수 호출시 어노테이션을 보고 감싸고 동작하기 때문에
    모든 어노테이션이 동작하지 않는걸로 알고 있습니다.
답글 달기