@Async
스프링에서는 @Async 어노테이션을 사용하면 비동기로 메서드를 호출할 수 있다.
@Async 어노테이션은 Spring AOP로 동작하며 요청 스레드와 별개의 스레드에서 해당 메서드를 비동기로 수행할 수 있게 한다.
ThreadPoolTaskExecutor 빈 등록
@EnableAsync 어노테이션도 같이 붙여야 @Async가 동작한다.
근데 왜 ThreadPoolTaskExecutor 빈을 등록하라는 걸까?
기본적으로 @EnableAsync 어노테이션을 사용하면 TaskExecutor 타입의 빈을 찾고 해당 빈을 통해 비동기를 처리한다.
만약 개발자가 직접 커스텀하게 등록한 TaskExecutor 빈이 존재하지 않으면 디폴트로 SimpleAsyncTaskExecutor를 사용하게 되는데, SimpleAsyncTaskExecutor는 스레드를 재사용하지 않고 계속해서 스레드를 생성한다.
즉, I/O 바운드인 스프링 부트 서버에 부하를 줄 수도 있다는 의미이다.
따라서 스레드 풀을 사용하는 ThreadPoolTaskExecutor 빈을 등록해주자.
참고로 SimpleAsyncTaskExecutor, ThreadPoolTaskExecutor는 TaskExecutor의 구현체이다.
ThreadPoolTaskExecutor가 무엇이길래?
ThreadPoolTaskExecutor는 TaskExecutor의 구현체로 ThreadPoolExecutor를 래핑한 스프링의 Executor이다.
ThreadPoolTaskExecutor 설정값
세 가지 개념만 이해하자. corePoolSize, queueCapacity, maxPoolSize
corePoolSize는 기본 스레드 풀의 크기를 정의한다. 이는 초기에 corePoolSize 만큼의 스레드를 생성한다.
만약 스레드 풀이 corePoolSize만큼 꽉 찼을 때 새로운 요청을 받으면 queueCapacity에 하나씩 요청이 쌓여 대기하게 된다.
스레드 풀이 queueCapacity 까지도 꽉찬 상태에서 새로운 요청을 받으면, 스레드 풀 사이즈를 maxPoolSize까지 1씩 증가시킨다.
스레드 풀이 1개씩 증가되면 queueCapacity에 저장된 테스크들을 1개씩 꺼내와 처리하게 된다.
어떻게 설정하는게 좋을까?
개발하는 서버가 CPU Bound 인지, I/O Bound인지에 따라 다르겠지만, corePoolSize는 일반적으로 예상되는 동시 작업의 수를 설정하는 것이 좋다.
너무 낮으면 작업이 큐에 대기하게 되고, 너무 높으면 자원을 비효율적으로 사용할 수 있기 때문이다.
queueCapacity는 충분히 큰 값을 설정해야 한다.
큐가 너무 작으면 maxPoolSize에 의존하게 되고, 너무 크면 스레드가 추가 생성되지 않아 작업이 대기 상태에 빠질 수 있다.
maxPoolSize는 예상되는 최대 동시 작업량을 기준으로 설정해야 하며, 너무 높으면 서버 자원 낭비가 발생할 수 있다.