Spring @Async

이주오·2022년 1월 19일
1

spring

목록 보기
4/4

발단

사용자가 어떤 게시글을 작성하면 조건에 맞는 다른 사용자에게 쪽지같은 알림을 구현해야 하는 상황

  • 게시글 작성
  • 알림

처음에는 하나의 transaction으로 처리로 구현을 진행했으나 알림 기능은 부가적인 기능이고 게시글 작성 기능에 영향을 주면 안된다고 생각이 들었다. 따라서 게시글 작성 후 알림 처리가 지연되는 경우 게시글 작성 자체를 지연하는 것이 아니라, 게시글 작성은 완료시키고 다른 Thread에서 알림을 처리하도록 비동기 처리를 진행할 수 있을 것이다.

스프링에서는 @Async Annotation을 이용하여 간단하게 비동기 처리를 할 수 있다.

Java 비동기방식 처리

그전에 먼저 자바의 비동기 작업 처리를 알아보자.

따라서 method가 실행되면 새로운 thread를 만들고 그 thread에서 메시지를 저장하도록 처리하면 될 것 같다.

public class AsyncService {
    public void asyncMethod(String message) throws Exception {
        // do something
    }
}

하지만 해당 방법은 thread를 관리할 수 없기 때문에 위험한 방법이다.

Thread를 관리하기 위해서 ExecutorService 사용해보자.

ExecutorService는 쉽게 비동기로 작업을 실행할 수 있도록 도와주는 JDK(1.5부터)에서 제공하는 interface 이다. 일반적으로 ExecutorService는 작업 할당을 위한 스레드 풀과 API를 제공한다.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class AsyncService {

    private static ExecutorService executorService = Executors.newFixedThreadPool(10);

    public void asyncMethod(final String message) throws Exception {
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                // do something
            }            
        });
    }

}

하지만 비동기방식으로 처리하고 싶은 method마다 반복적으로 동일한 수정 작업들을 진행 해야할 것이다.

@Async with SimpleAsyncTaskExecutor

@Async Annotation은 Spring에서 제공하는 Thread Pool을 활용하는 비동기 메소드 지원 Annotation이다.

만약 Spring Boot에서 간단히 사용하고 싶다면, 단순히 Application Class에 @EnableAsync Annotation을 추가하고, 원하는 method 위에 @Async Annotation을 붙여주면 사용할 수 있다.

@EnableAsync
@SpringBootApplication
public class SpringBootApplication {
    ...
}
public class AsyncService {

    @Async
    public void asyncMethod(String message) throws Exception {
        ....
    }
}

하지만 @Async의 기본설정은 SimpleAsyncTaskExecutor를 사용하도록 되어있기 때문입니다.

본인의 개발 환경에 맞게 Customize하기에는 직접 AsyncConfigurerSupport를 상속받는 Class를 작성하는 것이 좋다.

@Async with ThreadPoolTaskExecutor

Thread pool을 이용해서 thread를 관리가능한 방식다. 아래와 같은 AsyncConfigurerSupport를 상속받는 Customize Class를 구현하자.

그리고 Application 클래스에 @EnableAutoConfiguration(혹은 @SpringBootApplication) 설정이 되어있다면 런타임시 @Configuration가 설정된 SpringAsyncConfig 클래스의 threadPoolTaskExecutor bean 정보를 읽어들이기 때문에 앞서 설정한 Application 클래스의 @EnableAsync을 제거한다.

import java.util.concurrent.Executor;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurerSupport;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@EnableAsync
@Configuration
public class AsyncConfig extends AsyncConfigurerSupport {

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecuto();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(20);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("my thread-");
        executor.initialize();
        return executor;
    }
}

여기서 설정한 요소들은 아래와 같다.

  • @Configuration : Spring 설정 관련 Class로 @Component 등록되어 Scanning 될 수 있다.
  • @EnableAsync : Spring method에서 비동기 기능을 사용가능하게 활성화 한다.
  • CorePoolSize : 기본 실행 대기하는 Thread의 수**
  • MaxPoolSize : 동시 동작하는 최대 Thread의 수
  • QueueCapacity : MaxPoolSize 초과 요청에서 Thread 생성 요청시,해당 요청을 Queue에 저장하는데 이때 최대 수용 가능한 Queue의 수,Queue에 저장되어있다가 Thread에 자리가 생기면 하나씩 빠져나가 동작
  • ThreadNamePrefix : 생성되는 Thread 접두사 지정

위와 같이 작성한 후 비동기 방식 사용을 원하는 method에 @Async Annotation을 지정해주면 된다.

@Async annotation에 bean의 이름을 제공하면 SimpleAsyncTaskExecutor가 아닌 설정한 TaskExecutor로 thread를 관리하게 된다.

@EnableAsync
@Configuration
public class AsyncConfig extends AsyncConfigurerSupport {

    @Bean(name = "threadPoolTaskExecutor")
    public Executor threadPoolTaskExecutor()
    {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(3);
        taskExecutor.setMaxPoolSize(30);
        taskExecutor.setQueueCapacity(10);
        taskExecutor.setThreadNamePrefix("Executor-");
        taskExecutor.initialize();
        return taskExecutor;
    }
}

---

public class AsyncService {

		@Async("threadPoolTaskExecutor")
    public void asyncMethod(String message) throws Exception {
        // do something
    }
}

Thread Pool의 종류를 여러개 설정하고자한다면 SpringAsyncConfig 안에 bean을 여러개 만들고 @Async를 설정시 원하는 bean의 이름을 설정하면 된다.

제약사항

@Async Annotation을 사용할 때 아래와 같은 사항을 주의 해야한다.

  1. private method는 사용 불가, public method만 사용 가능
  2. self-invocation(자가 호출) 불가, 즉 inner method는 사용 불가

제약의 원인은 간단한데 @Async  은 AOP에 의해 동작하고 있기 때문에 해당 메서드는 프록시될 수 있어야 하기 때문이다.

https://media.vlpt.us/images/gillog/post/5bb64a29-5263-4fcc-9f02-cffea4162137/image.png

출처 : https://dzone.com/articles/effective-advice-on-spring-async-part-1

해당 @Async method를 가로챈 후, 다른 Class에서 호출이 가능해야 하므로,private method는 사용할 수 없는 것이다. 또한 inner method의 호출은 해당 메서드를 직접호출 하기 때문에  self-invocation이 불가능하다. @Transactional 사용시 주의점과 비슷하다고 할 수 있다.

@Slf4j
@Service
public class TestService {

    @Async
    public void innerAsyncMethod(int i) {
        log.info("async i = " + i);
    }

    public void asyncMethod(int i) {
        innerAsyncMethod(i);
    }
    
}

리턴 타입

@Async 메서드는 AsyncExecutionAspectSupport 클래스의 doSubmit 메서드에서 선택한 실행자와 함께 지정된 작업을 실제로 실행하도록 위임한다.

리턴타입은 크게 두가지로 나뉠 수 있다.

  • 리턴값이 없는 경우
  • 있는 경우(Futrue)

Future에 경우에도 여러 타입이 존재하는데 해당 리턴 값에 대한 것은 여기 에서 자세히 보면 좋을 것 같다

@Nullable
protected Object doSubmit(Callable<Object> task, AsyncTaskExecutor executor, Class<?> returnType) {
	if (CompletableFuture.class.isAssignableFrom(returnType)) {
		return CompletableFuture.supplyAsync(() -> {
			try {
				return task.call();
			}
			catch (Throwable ex) {
				throw new CompletionException(ex);
			}
		}, executor);
	}
	else if (ListenableFuture.class.isAssignableFrom(returnType)) {
		return ((AsyncListenableTaskExecutor) executor).submitListenable(task);
	}
	else if (Future.class.isAssignableFrom(returnType)) {
		return executor.submit(task);
	}
	else {
		executor.submit(task);
		return null;
	}
}

예외 처리

메서드 반환 형식이 Futre인 경우 예외 처리가 쉽다. Future.get() 메서드에서 예외가 발생하기 때문이다.

하지만 반환값이 없는 void이면 예외가 호출 스레드에 전파되지 않는다. 즉 해당 thread가 소리없이 죽기 때문에 예외 처리가 관리되지 않는다.

이러한 예외 처리를 위해서는 AsyncUncaughtExceptionHandler 인터페이스를 구현하여 사용자 지정 비동기 예외 처리기를 생성한다. handleUncaughtException() 메서드는 캐치되지 않은 비동기 예외가 있을 때 호출된다.


public class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {

    @Override
    public void handleUncaughtException(Throwable ex, Method method, Object... params) {
        log.warn("Exception message - {}", ex.getMessage());
        log.warn("Method name - {}", method.getName());
    }
}

추가

추후 해당 처리를 Spring event를 이용하거나 AOP를 이용하여 리팩토링도 진행할 예정이다.

참고 출처

profile
동료들이 같이 일하고 싶어하는 백엔드 개발자가 되고자 합니다!

0개의 댓글