@Async
Asynchronous excution을 지원하는 스프링 어노테이션이다.
빈에 등록된 함수에 이 하지만 이러한 방식은 @Async 어노테이션을 적용하면 요청을 별도의 스레드(같은 Object data를 공유하는 스레드 중 하나)에서 실행한다. 이러한 Async 동작방식을 적용하여 호출자는 해당 메서드가 완료되는 것을 기다릴 필요가 없다.
public interface EmailService {
@Async
void sendEmail(String template, Map<String, Object> properties);
}
→ 위 예시에서 sendEmail 메소드는 별도의 스레드에서 실행된다.
Spring’s @Async annotation, indicating that it should run on a separate thread. -spring.io
예전에 OS 과제로 Semaphore를 구현하다 알게된 사실이지만 사실 순수 Java만으로도 얼마든지 이러한 비동기/동기처리를 할 수 있다. 하지만 Java library나 class를 사용하여 직접 thread를 관리하다보면 Thread를 생성하고 관리, 중복되는 코드 등 비용이 막대하기에... (실제로 호출마다 Thread를 생성하고 또 다시 method를 호출하여 작업을 수행해야 한다.) Spring에서 제공하는 @Async와 thread pool을 활용하여 좀 더 효율적으로 개발을 할 수 있다.
@Async 특징
- Async를 적용하는 메서드의 접근제어자는 private이면 안된다. 비동기 메서드는 Spring AOP를 기반으로 동작하며, spring AOP가 메서드를 호출할 때 프록시 객체를 통해 호출되기 때문이다. 즉, private으로 다른 클래스의 접근을 통제해두면 프록시 객체가 해당 메서드에 접근할 수 없게되므로, public으로 선언되어야 한다.
- 이러한 spring AOP의 프록시가 관심사로 분류된 메소드를 호출할 때에는 Caller 메소드가 존재해야 한다. 즉, inner method에서는 동작하지 않는다. Bean에 등록된 method의 호출이어야 spring AOP가 관리하여 proxy 적용이 가능하기 때문이다.
spring AOP(Aspect-Object-Programming): 간단히 요악하자면 concern에 관한 코드를 모듈화하는 기술이다. 이러한 concern은 반복되는 코드, 로깅, 트랜잭션, 보안 등이 해당된다. AOP는 이러한 concern을 core(핵심 관심사)와 cross-cuttting(공통 관심사)로 분리하여 모듈화하고, 핵심 로직에서는 이러한 부가적인 내용을 신경쓰지 않도록 돕는다.
spring AOP는 어노테이션 기반/XML 설정을 통해 사용되며, 프록시 기반의 AOP를 사용하여 동작한다.
대상 객체를 감싸는 프록시 객체를 생성하고, 프록시 객체에서 어드바이스(실제로 수행되어야 하는 코드)를 호출함으로써 관점을 핵심 로직에 적용한다.
이러한 방식으로 핵심 로직은 자신의 비즈니스 로직에만 집중할 수 있다.
@Async 사용방식 2가지(SimpleAsyncTaskExecutor, TaskExecutor)
SimpleAsyncTaskExecutor
spring.io에 따르면 application단에 @EnableAsync를 선언하고, 비동기로 콜할 method에 @Async를 선언하면 된다.
하지만 이러한 방식은 @Async의 기본설정인 SimpleAsyncTaskExecutor를 사용하며,
이는 thread pool이 아닌 단순히 thread 하나를 만들어내는 역할을 하기에 thread를 직접 관리하지 않는다.
- org.springframework.core.task.SimpleAsyncTaskExecutor 패키지에 존재하며, 각 작업마다 새로운 스레드를 생성하고 비동기 방식으로 동작한다.
- 스레드 풀 방식이 아니므로, 스레드를 재사용하지 않는다. 이러한 이유로 공식 문서에서는 thread pool 방식의 TaskExecutor를 사용할 것은 권장한다. 특히 실행시간이 짧으면서 많은 량의 테스크를 처리할 때는 이 방식이 필요하다.
TaskExecutor
따라서 이번 포스팅에서는 TaskExecutor를 설정하고, thread pool 을 생성하여 @Async를 사용하는 방식에 대해 알아보자.
1. Config 파일에 @EnableAsync 어노테이션을 추가해준다.
필자는 src/main/java/example/config/CacheAndAsyncConfig에 작성하였다.
@Configuration
@EnableAsync // Async 설정 클래스에 해당 어노테이션 선언
public class AsyncConfig {
@Bean(name = "threadPoolTaskExecutor")
public Executor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(3); // 기본 스레드 수
executor.setMaxPoolSize(30); // 최대 스레드 수
executor.setQueueCapacity(100); // Queue 사이즈
executor.setThreadNamePrefix("Executor-");
return taskExecutor;
}
- orePoolSize: 스레드 풀에 넣어둘 기본 스레드 개수, default = 1
- queueCapacity: 대기 큐 크기, default = Integer.MAX_VALUE(약 21억)
- maxPoolSize: 최대 스레드 개수, default = Integer.MAX_VALUE(약 21억)
2. Service Method에서 @Async 사용하기
public class EmailService {
@Async("threadPoolTaskExecutor")
public void sendEmail(String message) {
....
}
}
thread pool을 사용하는 방법과 그 이유
thread pool이란?
스레드 풀은 쉽게 말해서 여러 스레드를 pool에 두고 관리하는 디자인 패턴/ 설계 방식이다.
미리 thread pool을 만들어 두고 위 그림과 같이 queue를 통한 task 처리로 concurrency를 가능하게 한다.
thread pool에는 FixedThreadPool(고정된 스레드 수), CachedThreadPool(필요한 만큼 생성)이 있다.
만약 @Async를 하나만 처리해도 되면 @EnableAsync-@Async를 활용하고,
그렇지 않다면 thread pool을 세팅하여 사용하는 것이 좋겠다.
실사용 예제로 알아보는 장점
사용자가 어떤 요청을 보낼 때 마다 우리 회사로 안내 메일이 들어오는 상황이 있다고 가정하자.
이는 회사 내부 업무의 편리함을 위해 설정한 포맷이며 사실상 이와 같은 기능이 없다고 하더라도 admin page에서 충분히 확인할 수 있는 내용이다. 즉, 업무 중요도 우선순위가 낮은 편이다.
이와 같이 우선순위가 낮은 작업은
@Async를 통해 비동기콜로 처리하면 새로운 스레드가 생성되어 해당 스레드가 api를 처리하게 된다.
이때 메인 스레드는 하고있던 역할을 수행할 수 있으므로 더 빠른 작업을 수행하도록 돕는 데 사용될 수 있다.
'Dev > Spring & JPA' 카테고리의 다른 글
[Spring] 처리율 제한(Rate Limit)으로 악의적인 공격 차단하기 (0) | 2024.11.23 |
---|---|
[Spring] HTTP Request Client(webclient, feignclient) (10) | 2024.11.10 |
[spring] null 처리를 위한 spring의 Stringutils (2) | 2023.12.21 |
[Backend] 객체 지향 특징 | 다형성 | 좋은 객체 지향 설계 5 가지 원칙(SOLID) | EJB (0) | 2023.07.19 |