Async

EnableAsync, Async 어노테이션으로 적용

비동기로 돌아가야하는 서비스가 있다면 그 앞에 @Async 어노테이션을 붙인 후에,
스프링부트 어플리케이션 클래스에 @EnableAsync 어노테이션을 붙여주면 비동기로 동작한다.

@EnableAsync
@SpringBootApplication
public class AsyncApplication {

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

}
@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
@Slf4j
public class ApiController {

    private final AsyncService asyncService;

    @GetMapping("/hello")
    public String hello(){
        asyncService.hello();
        log.info("method end");
        return "hello";
    }

}
@Service
@Slf4j
public class AsyncService {

    @Async
    //Async 어노테이션이 붙어있기 때문에 아래 메소드는 비동기로 작동한다.
    public void hello() {
        for(int i =0; i<10; i++){
            try{
                Thread.sleep(2000);
                log.info("thread sleep ...");
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
    }
}

/api/hello에 요청을 보내보면 다음과 같이 로그가 찍힌다.

자세히 보면 nio-8080-exec-1 스레드가 아닌 task-1 스레드에서 동작하는것을 확인할 수 있다.

그럼 이런 경우에, Response를 어떻게 줄까?

Async에 대한 Response

@Async를 붙인 메소드의 반환값을 받아서 CompletableFuture를 반환하는 메소드를 만들어준다.

@EnableAsync
@SpringBootApplication
public class AsyncApplication {

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

}
@Service
@Slf4j
public class AsyncService {

    @Async
    public CompletableFuture run(){
        return new AsyncResult(hello()).completable();
    }

    public String hello() {
        for(int i =0; i<10; i++){
            try{
                Thread.sleep(2000);
                log.info("thread sleep ...");
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
        return "async hello";
    }
}

그 후, 컨트롤러에서 CompletableFuture를 반환받아 return하도록 변경한다.

@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
@Slf4j
public class ApiController {

    private final AsyncService asyncService;

    @GetMapping("/hello")
    public CompletableFuture hello(){
        log.info("completable future init");
        return asyncService.run();
    }

}

이렇게 하면, Async동작이 모두 완료된 후에 응답이 내려지게 된다.
사실 이 예제에서는 CompletableFuture가 하는 역할이 그렇게 중요하지 않다. 비동기로 동작하는 것을 그냥 원래처럼 동기로 동작하도록 돌려서 결과를 반환받아도 동일하기 때문이다.
하지만, 예를 들어 여러개의 API에 요청을 보내고 그 결과값을 받아서 하나의 서비스 로직에 사용하려는 경우에 유용하게 사용할 수 있다.
왜냐하면 하나씩 차례로 요청을 보내기 보다는 한꺼번에 비동기로 요청을 보낸 후, 그 결과값이 전부 도착했을때, 혹은 도착한 후 정해진 순서에 따라 로직을 실행시키면 동기로 처리할 때보다 처리 속 도가 더 빨라지기 때문이다.
주의해야할 점은 스프링에서 관리하는 스레드 갯수는 정해져있다는 점이다.
만약 별도 스레드를 직접 만들어서 비동기로 돌리고 싶다면 다음과 같이 한다.

직접 스레드 만들어 사용하기

비동기를 처리할 스레드를 설정하는 Configuration Bean을 등록하자.

@Configuration
public class AppConfig {

    @Bean("async-thraed")
    public Executor asyncThread(){
        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
        //3. 그렇게 점점 쌓인 코어 갯수의 상한선이 100개이다.
        threadPoolTaskExecutor.setMaxPoolSize(100);
        //1. 코어를 10개 사용하는데 이게 다 차게되면
        threadPoolTaskExecutor.setCorePoolSize(10);
        //2. 큐에 쌓이게 된다. 이게 다 쌓이게 되면 다시 코어가 10개 늘어난다.
        threadPoolTaskExecutor.setQueueCapacity(10);
        threadPoolTaskExecutor.setThreadNamePrefix("Async-");
        return threadPoolTaskExecutor;

    }
}

해당 Executor로 비동기를 적용하려면 @Async어노테이션에 빈이름만 설정해주면 된다.

@Service
@Slf4j
public class AsyncService {

    @Async("async-thread") //비동기 처리자 할당
    public CompletableFuture run(){
        return new AsyncResult(hello()).completable();
    }

//    @Async
    //Async 어노테이션이 붙어있기 때문에 아래 메소드는 비동기로 작동한다.
    public String hello() {
        for(int i =0; i<10; i++){
            try{
                Thread.sleep(2000);
                log.info("thread sleep ...");
            }catch (InterruptedException e){
                e.printStackTrace();
            }
        }
        return "async hello";
    }
}

아까 task-1 스레드에서 동작하던 것이 이제 우리가 설정한 이름인 Async- 스레드 prepend가 붙은 이름으로 보이게 된다.

추가 주의사항

@Async는 AOP기반이기 때문에 프록시패턴에 따라 작동하며 public메소드에만 붙여줄 수 있다.

@Async작동하게 한 run()메소드에서 직접적으로 hello()를 호출하면 비동기로 동작하지 않는다. 왜냐하면 같은 클래스내의 메소드를 호출하면 @Async가 동작하지 않으므로.