2carrot84
by 2carrot84
9 min read

Categories

  • development

Tags

  • annotation
  • async
  • concurrent
  • spring

오늘은 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 동작 하지 않는다

  • 주의점
    • Exception Handling
      • @Async 메서드에 발생하는 예외는 호출자에게 전파되지 않는다. (별도의 thread 실행)
      • AsyncUncaughtExceptionHandler 를 구현하여 예외처리 또는 CompletableFuture 의 exceptionally() 메소드 를 활용한 예외처리가 필요
    • Method 호출
      • 내부 호출 시 async 동작 하지 않는다.
    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: 익셉션!

commit 성공

위와 같이 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 등에 대해서도 간략히 테스트 코드와 함께 포스팅 하도록 하겠습니다.

그럼 이만. 🥕👋🏼🖐🏼

참고자료🤣

스프링에서 @Async를 사용할때 주의점
[Spring] @Async로 비동기 처리하기