오늘은 spring 에서 제공하는 @Async 어노테이션을 통한 비동기 처리에 대해 간략히 정리해 보려고 합니다.
@Async
어노테이션을 통해 간단하게 비동기 메서드를 실행
@Async 어노테이션 사용을 위해선 @EnableAsync 어노테이션을 먼저 붙혀줘야 한다.
@EnableAsync
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
@Slf4j
@Service
public class CallService {
private final AsyncService asyncService;
public CallService(AsyncService asyncService) {
this.asyncService = asyncService;
}
public void callAsync() {
log.info(">>>>> callAsync!!");
asyncService.async1();
asyncService.async2();
}
}
@Slf4j
@Service
public class AsyncService {
@Async
public void async1() {
log.info(">>>>> async1()");
for (int i = 0; i < 5; i++) {
log.info(">>>>> Thread Name : " + Thread.currentThread().getName());
}
}
@Async
public void async2() {
log.info(">>>>> async2()");
for (int i = 0; i < 5; i++) {
log.info(">>>>> Thread Name : " + Thread.currentThread().getName());
}
}
}
2024-10-26T15:28:19.029+09:00 INFO 3614 --- [demo] [ Test worker] com.example.demo.async.CallService : >>>>> callAsync!!
2024-10-26T15:28:19.034+09:00 INFO 3614 --- [demo] [ taskExecutor-1] com.example.demo.async.AsyncService : >>>>> async1()
2024-10-26T15:28:19.034+09:00 INFO 3614 --- [demo] [ taskExecutor-2] com.example.demo.async.AsyncService : >>>>> async2()
2024-10-26T15:28:19.034+09:00 INFO 3614 --- [demo] [ taskExecutor-1] com.example.demo.async.AsyncService : >>>>> Thread Name : taskExecutor-1
2024-10-26T15:28:19.034+09:00 INFO 3614 --- [demo] [ taskExecutor-1] com.example.demo.async.AsyncService : >>>>> Thread Name : taskExecutor-1
2024-10-26T15:28:19.034+09:00 INFO 3614 --- [demo] [ taskExecutor-2] com.example.demo.async.AsyncService : >>>>> Thread Name : taskExecutor-2
2024-10-26T15:28:19.034+09:00 INFO 3614 --- [demo] [ taskExecutor-1] com.example.demo.async.AsyncService : >>>>> Thread Name : taskExecutor-1
2024-10-26T15:28:19.035+09:00 INFO 3614 --- [demo] [ taskExecutor-2] com.example.demo.async.AsyncService : >>>>> Thread Name : taskExecutor-2
2024-10-26T15:28:19.035+09:00 INFO 3614 --- [demo] [ taskExecutor-2] com.example.demo.async.AsyncService : >>>>> Thread Name : taskExecutor-2
2024-10-26T15:28:19.035+09:00 INFO 3614 --- [demo] [ taskExecutor-2] com.example.demo.async.AsyncService : >>>>> Thread Name : taskExecutor-2
2024-10-26T15:28:19.035+09:00 INFO 3614 --- [demo] [ taskExecutor-2] com.example.demo.async.AsyncService : >>>>> Thread Name : taskExecutor-2
2024-10-26T15:28:19.035+09:00 INFO 3614 --- [demo] [ taskExecutor-1] com.example.demo.async.AsyncService : >>>>> Thread Name : taskExecutor-1
2024-10-26T15:28:19.035+09:00 INFO 3614 --- [demo] [ taskExecutor-1] com.example.demo.async.AsyncService : >>>>> Thread Name : taskExecutor-1
위 코드를 실행해 보면 위와 같이
각 taskExecutor-1 와 taskExecutor-2 스레드에서 비동기 적으로 async1, async2 메소드가 실행되는 걸 확인
할 수 있다.
@Async 를 사용하지 않으면 아래와 같은 결과를 볼 수 있다.
2024-10-26T15:32:36.270+09:00 INFO 3755 --- [demo] [ Test worker] com.example.demo.async.CallService : >>>>> callAsync!!
2024-10-26T15:32:36.270+09:00 INFO 3755 --- [demo] [ Test worker] com.example.demo.async.AsyncService : >>>>> async1()
2024-10-26T15:32:36.270+09:00 INFO 3755 --- [demo] [ Test worker] com.example.demo.async.AsyncService : >>>>> Thread Name : Test worker
2024-10-26T15:32:36.270+09:00 INFO 3755 --- [demo] [ Test worker] com.example.demo.async.AsyncService : >>>>> Thread Name : Test worker
2024-10-26T15:32:36.270+09:00 INFO 3755 --- [demo] [ Test worker] com.example.demo.async.AsyncService : >>>>> Thread Name : Test worker
2024-10-26T15:32:36.270+09:00 INFO 3755 --- [demo] [ Test worker] com.example.demo.async.AsyncService : >>>>> Thread Name : Test worker
2024-10-26T15:32:36.270+09:00 INFO 3755 --- [demo] [ Test worker] com.example.demo.async.AsyncService : >>>>> Thread Name : Test worker
2024-10-26T15:32:36.271+09:00 INFO 3755 --- [demo] [ Test worker] com.example.demo.async.AsyncService : >>>>> async2()
2024-10-26T15:32:36.271+09:00 INFO 3755 --- [demo] [ Test worker] com.example.demo.async.AsyncService : >>>>> Thread Name : Test worker
2024-10-26T15:32:36.271+09:00 INFO 3755 --- [demo] [ Test worker] com.example.demo.async.AsyncService : >>>>> Thread Name : Test worker
2024-10-26T15:32:36.271+09:00 INFO 3755 --- [demo] [ Test worker] com.example.demo.async.AsyncService : >>>>> Thread Name : Test worker
2024-10-26T15:32:36.271+09:00 INFO 3755 --- [demo] [ Test worker] com.example.demo.async.AsyncService : >>>>> Thread Name : Test worker
2024-10-26T15:32:36.271+09:00 INFO 3755 --- [demo] [ Test worker] com.example.demo.async.AsyncService : >>>>> Thread Name : Test worker
Test worker 란 스레드에서 async1 이 실행된 이후 async2 메소드가 순차적으로 실행되는걸 확인
할 수 있다.
- ThreadExecutor 로 default 로 SimpleAsyncTaskExecutor 를 사용
- 각 작업마다 새로운 스레드를 생성하고 비동기 방식으로 동작
- concurrency limit 프로퍼티를 이용해 지정한 수 보다 요청이 넘어설 경우 제한을 걸 수 있다.
- 작동 방식
- spring AOP 에 의해 Proxy 방식으로 작동
- Async Bean 이 등록되는 시점에 Proxy 객체화 하여 등록
- 호출한 객체는 Proxy 객체화된 Async Bean 참조
private method, inner-method 호출 시 Async 동작 하지 않는다
- 호출한 객체는 Proxy 객체화된 Async Bean 참조
- 주의점
- Exception Handling
- @Async 메서드에 발생하는 예외는 호출자에게 전파되지 않는다. (별도의 thread 실행)
- AsyncUncaughtExceptionHandler 를 구현하여 예외처리 또는 CompletableFuture 의 exceptionally() 메소드 를 활용한 예외처리가 필요
- Method 호출
- 내부 호출 시 async 동작 하지 않는다.
- Exception Handling
public void callInnerAsync() {
log.info("[내부 클래스의 Async 메소드 호출]");
this.async1();
this.async2();
}
2024-10-26T15:40:12.606+09:00 INFO 3933 --- [demo] [ Test worker] com.example.demo.async.AsyncService : [내부 클래스의 Async 메소드 호출]
2024-10-26T15:40:12.606+09:00 INFO 3933 --- [demo] [ Test worker] com.example.demo.async.AsyncService : >>>>> async1()
2024-10-26T15:40:12.606+09:00 INFO 3933 --- [demo] [ Test worker] com.example.demo.async.AsyncService : >>>>> Thread Name : Test worker
2024-10-26T15:40:12.606+09:00 INFO 3933 --- [demo] [ Test worker] com.example.demo.async.AsyncService : >>>>> Thread Name : Test worker
2024-10-26T15:40:12.606+09:00 INFO 3933 --- [demo] [ Test worker] com.example.demo.async.AsyncService : >>>>> Thread Name : Test worker
2024-10-26T15:40:12.606+09:00 INFO 3933 --- [demo] [ Test worker] com.example.demo.async.AsyncService : >>>>> Thread Name : Test worker
2024-10-26T15:40:12.606+09:00 INFO 3933 --- [demo] [ Test worker] com.example.demo.async.AsyncService : >>>>> Thread Name : Test worker
2024-10-26T15:40:12.606+09:00 INFO 3933 --- [demo] [ Test worker] com.example.demo.async.AsyncService : >>>>> async2()
2024-10-26T15:40:12.606+09:00 INFO 3933 --- [demo] [ Test worker] com.example.demo.async.AsyncService : >>>>> Thread Name : Test worker
2024-10-26T15:40:12.606+09:00 INFO 3933 --- [demo] [ Test worker] com.example.demo.async.AsyncService : >>>>> Thread Name : Test worker
2024-10-26T15:40:12.607+09:00 INFO 3933 --- [demo] [ Test worker] com.example.demo.async.AsyncService : >>>>> Thread Name : Test worker
2024-10-26T15:40:12.607+09:00 INFO 3933 --- [demo] [ Test worker] com.example.demo.async.AsyncService : >>>>> Thread Name : Test worker
2024-10-26T15:40:12.607+09:00 INFO 3933 --- [demo] [ Test worker] com.example.demo.async.AsyncService : >>>>> Thread Name : Test worker
- 리턴 타입
- void, Future, CompletableFuture 중 하나의 반환 타입을 가져야 한다.
- Future, CompletableFuture 는 non-blocking 방식으로 접근, 필요시 callback 사용 처리
@Async
public String async3() {
log.info(">>>>> async3()");
for (int i = 0; i < 5; i++) {
log.info(">>>>> Thread Name : " + Thread.currentThread().getName());
}
return "Hello, world";
}
public void callAsyncReturnString() {
log.info(">>>>> callAsyncReturnString");
System.out.println(asyncService.async3());
}
2024-10-26T15:44:28.456+09:00 INFO 4162 --- [demo] [ Test worker] com.example.demo.async.CallService : >>>>> callAsyncReturnString
Invalid return type for async method (only Future and void supported): class java.lang.String
java.lang.IllegalArgumentException: Invalid return type for async method (only Future and void supported): class java.lang.String
at org.springframework.aop.interceptor.AsyncExecutionAspectSupport.doSubmit(AsyncExecutionAspectSupport.java:301)
위와 같이 String 타입을 리턴하게 하면 위의 에러가 발생하는 걸 확인할 수 있습니다.
@Async
public CompletableFuture<String> asyncReturnCompletableFuture() {
log.info(">>>>> asyncReturnCompletableFuture()");
for (int i = 0; i < 5; i++) {
log.info(">>>>> Thread Name : " + Thread.currentThread().getName() + ", " + i);
}
return CompletableFuture.completedFuture("Hello, world!!");
}
public void callAsyncReturnCompleteFuture() {
CompletableFuture<String> completableFuture = asyncService.asyncReturnCompletableFuture();
completableFuture.thenApply(result -> {
log.info(">>>>> Thread Name : " + Thread.currentThread().getName() + " result : " + result + ", " + LocalDateTime.now());
return result;
}).exceptionally(e -> {
log.error("예외 발생" + e.getMessage());
return "";
});
}
2024-10-26T15:48:41.420+09:00 INFO 4264 --- [demo] [ taskExecutor-1] com.example.demo.async.AsyncService : >>>>> asyncReturnCompletableFuture()
2024-10-26T15:48:41.420+09:00 INFO 4264 --- [demo] [ taskExecutor-1] com.example.demo.async.AsyncService : >>>>> Thread Name : taskExecutor-1, 0
2024-10-26T15:48:41.420+09:00 INFO 4264 --- [demo] [ taskExecutor-1] com.example.demo.async.AsyncService : >>>>> Thread Name : taskExecutor-1, 1
2024-10-26T15:48:41.420+09:00 INFO 4264 --- [demo] [ taskExecutor-1] com.example.demo.async.AsyncService : >>>>> Thread Name : taskExecutor-1, 2
2024-10-26T15:48:41.420+09:00 INFO 4264 --- [demo] [ taskExecutor-1] com.example.demo.async.AsyncService : >>>>> Thread Name : taskExecutor-1, 3
2024-10-26T15:48:41.421+09:00 INFO 4264 --- [demo] [ taskExecutor-1] com.example.demo.async.AsyncService : >>>>> Thread Name : taskExecutor-1, 4
2024-10-26T15:48:41.421+09:00 INFO 4264 --- [demo] [ taskExecutor-3] com.example.demo.async.AsyncService : >>>>> asyncReturnCompletableFuture()
2024-10-26T15:48:41.421+09:00 INFO 4264 --- [demo] [ taskExecutor-2] com.example.demo.async.AsyncService : >>>>> asyncReturnCompletableFuture()
2024-10-26T15:48:41.421+09:00 INFO 4264 --- [demo] [ taskExecutor-1] com.example.demo.async.CallService : >>>>> Thread Name : taskExecutor-1 result : Hello, world!!, 2024-10-26T15:48:41.421313
2024-10-26T15:48:41.421+09:00 INFO 4264 --- [demo] [ taskExecutor-3] com.example.demo.async.AsyncService : >>>>> Thread Name : taskExecutor-3, 0
2024-10-26T15:48:41.421+09:00 INFO 4264 --- [demo] [ taskExecutor-2] com.example.demo.async.AsyncService : >>>>> Thread Name : taskExecutor-2, 0
2024-10-26T15:48:41.421+09:00 INFO 4264 --- [demo] [ taskExecutor-3] com.example.demo.async.AsyncService : >>>>> Thread Name : taskExecutor-3, 1
2024-10-26T15:48:41.422+09:00 INFO 4264 --- [demo] [ taskExecutor-2] com.example.demo.async.AsyncService : >>>>> Thread Name : taskExecutor-2, 1
2024-10-26T15:48:41.422+09:00 INFO 4264 --- [demo] [ taskExecutor-2] com.example.demo.async.AsyncService : >>>>> Thread Name : taskExecutor-2, 2
2024-10-26T15:48:41.422+09:00 INFO 4264 --- [demo] [ taskExecutor-3] com.example.demo.async.AsyncService : >>>>> Thread Name : taskExecutor-3, 2
2024-10-26T15:48:41.422+09:00 INFO 4264 --- [demo] [ taskExecutor-2] com.example.demo.async.AsyncService : >>>>> Thread Name : taskExecutor-2, 3
2024-10-26T15:48:41.422+09:00 INFO 4264 --- [demo] [ taskExecutor-2] com.example.demo.async.AsyncService : >>>>> Thread Name : taskExecutor-2, 4
2024-10-26T15:48:41.422+09:00 INFO 4264 --- [demo] [ taskExecutor-3] com.example.demo.async.AsyncService : >>>>> Thread Name : taskExecutor-3, 3
2024-10-26T15:48:41.422+09:00 INFO 4264 --- [demo] [ taskExecutor-3] com.example.demo.async.AsyncService : >>>>> Thread Name : taskExecutor-3, 4
2024-10-26T15:48:41.422+09:00 INFO 4264 --- [demo] [ taskExecutor-2] com.example.demo.async.CallService : >>>>> Thread Name : taskExecutor-2 result : Hello, world!!, 2024-10-26T15:48:41.422571
2024-10-26T15:48:41.422+09:00 INFO 4264 --- [demo] [ taskExecutor-3] com.example.demo.async.CallService : >>>>> Thread Name : taskExecutor-3 result : Hello, world!!, 2024-10-26T15:48:41.422776
CompletableFuture 과 thenApply 메소드를 이용하면, non-blocking 방식으로 리턴값을 받을 수 있습니다.
- 트랜잭션 관리
- 비동기 메서드 내에서 트랜잭션은 호출한 메서드의 트랜잭션과는 별개의 생명주기를 가진다.
- @Async 붙은 메서드에서 rollback 이 발생해도 호출한 메서드는 commit 된다.
@Transactional
public void transactionalMethod() {
log.info(">>>>> tx.name : " + TransactionSynchronizationManager.getCurrentTransactionName());
log.info(">>>>> tx.isActive : " + TransactionSynchronizationManager.isActualTransactionActive());
asyncService.asyncWithTransactional();
asyncService.noAsyncWithTransactional();
}
@Async
@Transactional
public void asyncWithTransactional() {
log.info(">>>>> asyncWithTransactional()");
log.info(">>>>> asyncWithTransactional tx.name : " + TransactionSynchronizationManager.getCurrentTransactionName());
log.info(">>>>> asyncWithTransactional tx.isActive : " + TransactionSynchronizationManager.isActualTransactionActive());
}
@Transactional
public void noAsyncWithTransactional() {
log.info(">>>>> noAsyncWithTransactional()");
log.info(">>>>> noAsyncWithTransactional tx.name : " + TransactionSynchronizationManager.getCurrentTransactionName());
log.info(">>>>> noAsyncWithTransactional tx.isActive : " + TransactionSynchronizationManager.isActualTransactionActive());
}
2024-10-26T15:58:58.316+09:00 INFO 4515 --- [demo] [ Test worker] com.example.demo.async.CallService : >>>>> tx.name : com.example.demo.async.CallService.transactionalMethod
2024-10-26T15:58:58.316+09:00 INFO 4515 --- [demo] [ Test worker] com.example.demo.async.CallService : >>>>> tx.isActive : true
2024-10-26T15:58:58.318+09:00 INFO 4515 --- [demo] [ Test worker] com.example.demo.async.AsyncService : >>>>> noAsyncWithTransactional()
2024-10-26T15:58:58.319+09:00 INFO 4515 --- [demo] [ Test worker] com.example.demo.async.AsyncService : >>>>> noAsyncWithTransactional tx.name : com.example.demo.async.CallService.transactionalMethod
2024-10-26T15:58:58.319+09:00 INFO 4515 --- [demo] [ taskExecutor-1] com.example.demo.async.AsyncService : >>>>> asyncWithTransactional()
2024-10-26T15:58:58.319+09:00 INFO 4515 --- [demo] [ Test worker] com.example.demo.async.AsyncService : >>>>> noAsyncWithTransactional tx.isActive : true
2024-10-26T15:58:58.319+09:00 INFO 4515 --- [demo] [ taskExecutor-1] com.example.demo.async.AsyncService : >>>>> asyncWithTransactional tx.name : com.example.demo.async.AsyncService.asyncWithTransactional
2024-10-26T15:58:58.319+09:00 INFO 4515 --- [demo] [ taskExecutor-1] com.example.demo.async.AsyncService : >>>>> asyncWithTransactional tx.isActive : true
위와 같이 @Async 가 없는 메서드는
noAsyncWithTransactional tx.name : com.example.demo.async.CallService.transactionalMethod
와 같이 Default 전파 옵션인 REQUIRED 에 따라 호출자의 트랜잭션에 참여하나, @Async 와 @Transactional 이 붙은 메서드는asyncWithTransactional tx.name : com.example.demo.async.AsyncService.asyncWithTransactional
와 같이 새로운 트랜잭션을 가지는 것을 볼 수 있다.
@Slf4j
@Service
public class ProductService {
private final ProductRepository repository;
public ProductService(ProductRepository repository) {
this.repository = repository;
}
@Transactional
public Product save(Product product) {
log.info(">>>>> ProductService.save tx.name : " + TransactionSynchronizationManager.getCurrentTransactionName());
return repository.saveAndFlush(product);
}
}
@Transactional
public void saveAndNoException() {
log.info(">>>>> CallService.saveAndNoException tx.name : "
+ TransactionSynchronizationManager.getCurrentTransactionName());
productService.save(Product.builder()
.name("등록성공")
.price(10000)
.build());
asyncService.saveAndException();
}
@Async
@Transactional
public void saveAndException() {
log.info(">>>>> AsyncService.saveAndException tx.name : " + TransactionSynchronizationManager.getCurrentTransactionName());
productService.save(Product.builder()
.name("미등록")
.price(10000)
.build());
throw new RuntimeException("익셉션!");
}
2024-10-26T16:06:38.658+09:00 INFO 4635 --- [demo] [nio-8080-exec-1] com.example.demo.async.CallService : >>>>> CallService.saveAndNoException tx.name : com.example.demo.async.CallService.saveAndNoException
2024-10-26T16:06:38.658+09:00 INFO 4635 --- [demo] [nio-8080-exec-1] com.example.demo.jpa.ProductService : >>>>> ProductService.save tx.name : com.example.demo.async.CallService.saveAndNoException
Hibernate: insert into product (category,name,price,id) values (?,?,?,default)
2024-10-26T16:06:38.723+09:00 INFO 4635 --- [demo] [ taskExecutor-1] com.example.demo.async.AsyncService : >>>>> AsyncService.saveAndException tx.name : com.example.demo.async.AsyncService.saveAndException
2024-10-26T16:06:38.723+09:00 INFO 4635 --- [demo] [ taskExecutor-1] com.example.demo.jpa.ProductService : >>>>> ProductService.save tx.name : com.example.demo.async.AsyncService.saveAndException
Hibernate: insert into product (category,name,price,id) values (?,?,?,default)
2024-10-26T16:06:38.727+09:00 ERROR 4635 --- [demo] [ taskExecutor-1] .a.i.SimpleAsyncUncaughtExceptionHandler : Unexpected exception occurred invoking async method: public void com.example.demo.async.AsyncService.saveAndException()
java.lang.RuntimeException: 익셉션!
위와 같이
AsyncService.saveAndException tx.name : com.example.demo.async.AsyncService.saveAndException
별도 트랜잭션을 생성하여, 익셉션 발생으로 rollback 이 되었지만,com.example.demo.async.CallService.saveAndNoException
은 commit 이 되어 DB 상에 데이터가 정상 등록된걸 확인할 수 있습니다.
- Executor
- 기본적으로 simpleAsyncTaskExecutor 사용, 작업마다 새로운 스레드 생성 (자원 비효율)
- ThreadPoolTaskExecutor 사용 권장
@EnableAsync
@Configuration
public class AsyncConfig {
@Bean(name = "taskExecutor")
public ThreadPoolTaskExecutor executor(){
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setQueueCapacity(20);
executor.setMaxPoolSize(10);
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
}
@Async("taskExecutor")
ThreadPoolTaskExecutor 사용을 위해서는 별도의 Config 클래스를 만들어 위와 같이 사용하면 됩니다.
마무리
@Async 어노테이션과 간략하게 주변 지식에 대해 학습해 볼 수 있는 시간이였습니다.
실무에서 아직 비동기 처리는 경험하지 못 해서 빨리 실무에서 다양한 비동기 처리 방식을 사용해 보고 싶은 마음이 드네요.
추후 ThreadPoolTaskExecutor 과 Future, CompletableFuture 등에 대해서도 간략히 테스트 코드와 함께 포스팅 하도록 하겠습니다.
그럼 이만. 🥕👋🏼🖐🏼