Spring Async 사용하기

박은빈·2023년 2월 28일
0

자바

목록 보기
12/25

만약 자바스크립트를 배워본적이 있다면 비동기 논블로킹이라는 개념을 잘 알고있을것이다
그리고 자바스크립트의 핵심기능인 Async도 잘 알고있을 것이다

난 이게 자바에는 없는줄 알았는데 Spring 기능중에 Async라는 기능이 존재했었고

그 기능에 대해 살짝 알아본걸 여기에 적어본다

spring async란 thread pool을 이용해 비동기 처리를 진행하는 어노테이션이다

기존의 자바를 이용해서도 처리가 가능했지만 async를 이용하면 간편하게 처리가 가능하다

사용법

기본적으로 비동기로 사용하고 싶은 메서드가 있는 클래스에 @EnableAsync를 붙이고
메서드에 @Async를 붙인다

@EnableAsync
public class Service {

	@Async
    public int async() {
    	
    }
}

이렇게 설정하면 스프링부트에서 간단히 비동기 논 블로킹 처리가 가능하다

간단히 테스트를 해보면

public void asyncTest() {
        log.info("메서드 시작");
        async.async();
        log.info("메서드 끝");
}
@Async
public void async() {
    log.info("비동기 작업 처리 시작");
    for (int i=0; i<100000; i++) {}
    log.info("비동기 작업 처리 완료");
}

보통 자바라면 하나씩 실행하기때문에 for에 있는 i를 10만번 더하는 작업이 완료된 후 메서드 끝이 출력되어야한다
하지만 결과는 다르게 나온다

비동기 논 블로킹 처리가 이루어져서 메서드가 시작하자마자 끝으로 넘어가고
다른 스레드에서 비동기 작업을 처리한다

이렇게 간단하게 비동기 논블로킹을 구현했다

스레드 풀 설정

하지만 각 서버마다 cpu와 ram의 성능이 다르고 사람마다 스레드 풀을 설정하고싶은게 다르다
하지만 이렇게 기본적으로 @EnableAsync를 써서 구현하면 스레드 풀을 스프링에서 설정한 기본값으로 작동하게 된다 하지만 기본값은 최적화된 설정이 아니기때문에 작성자가 직접 설정을 해줄수 있다

ThreadPoolTaskExecutor를 이용해 직접 @Async에 대한 스레드 풀 설정을 해줄 수 있다

@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean
    public Executor asyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(50);
        executor.setQueueCapacity(100);
        executor.setKeepAliveSeconds(120);
        executor.setAllowCoreThreadTimeOut(true);
        executor.setPrestartAllCoreThreads(true);
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setAwaitTerminationSeconds(20);
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        executor.setThreadNamePrefix("Async-Test");
        executor.initialize();
        
        return executor;
    }
}

설정이 아주 다양하고 자세하다. 하나씩 살펴보자

  • setCorePoolSize() : 기본 스레드 사이즈를 설정한다
  • setMaxPoolSize() : 최대 스레드 사이즈를 설정한다
  • setQueueCapacity() : 스레드 대기 큐의 사이즈를 설정한다
  • setKeepAliveSeconds() : 해당 초까지 idle상태가 유지되면 스레드를 종료한다 (idle:어떠한 동작 상태도 아닐때)
  • setAllowCoreThreadTimeOut() : setKeepAliveSeconds에 대한 활성화 여부
  • setPrestartAllCoreThreads() : 작업 이전에 스레드를 활성화 시킬지에 대한 여부
  • setWaitForTasksToCompleteOnShutdown() : 진행중이던 작업이 완료 된 후 스레드를 종료시킬지에 대한 여부
  • setAwaitTerminationSeconds() : 작업을 몇초동안 기다려줄지에 대한 여부
  • setRejectedExecutionHandler() : 스레드 작업중 예외가 발생했을대 핸들링 여부 위에처럼 기본으로 나와있는 정책들을 사용할 수도 있고 커스텀 정책들도 사용이 가능하다 자세한건 벨덩에서 확인
    (https://www.baeldung.com/java-rejectedexecutionhandler)
  • setThreadNamePrefix() : 스레드의 이름 지정

설정들을 잘 활용하면 최적의 스레드를 사용해 비동기 처리를 사용할 수 있다

여기서 만약 두개의 @Async메서드를 생성하고 둘 다 설정을 다르게 해주고 싶을때는 어떻게 할까?

방법은 bean에 해당 Async 메서드의 이름을 달면 된다

@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean(name = "testAsync")
    public Executor asyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5);
        return executor;
    }
}

Async 콜백

메서드를 실행하고 리턴값이 없다면 괜찮지만 만약 리턴값이 있다면 어떻게 해야할까
바로 콜백을 이용하면된다

일반적인 리턴을 할 경우 비동기 논블로킹으로 동작하지않기 때문에 Future라는 객체를 이용해 콜백을 시켜준다

Future에는 Future,ListenableFutue,CompletableFuture등이 존재하는데
스프링부트3에서는 CompletableFuture가 동작하므로 나는 이 객체를 사용했다

@Async
public CompletableFuture<String> async() {
    log.info("비동기 작업 처리 시작");

    int i;
    for (i=0; i<100000; i++) {}

    log.info("비동기 작업 처리 완료");

    String futureStr = i + "번 for문 반복 완료";

    return CompletableFuture.completedFuture(futureStr);
}

Async메서드에 CompletableFutre라는 객체를 리턴받게하고 그 안에 futureStr이라는 String을 넣었다

public String asyncTest() {

    log.info("메서드 시작");

    CompletableFuture<String> future = async.async();
    future.thenAccept(str -> {
        log.info("리턴값 콜백 완료: " + str);
    });

    log.info("메서드 끝");

    return "index";
}

그리고 실행할 메서드에서 값을 CompletableFuture로 받고 thenAccept를 이용해 정상적으로 받아진 값을 람다식을 이용해 처리하였다

일반적으로 Async를 이용하지않은 코드일경우에는 log가 밑에처럼 뜰것이다

1. 메서드 시작
2. 비동기 작업 처리 시작
3. 비동기 작업 처리 끝
4. 리턴값 콜백 완료: 1000000번 for문 반복 완료
5. 메서드 끝

하지만 CompletableFuture를 이용해 콜백 형식으로 받으면 다음과 같은 결과가 나온다

이렇게 콜백 패턴이 정상적으로 적용된 것을 볼 수 있다.

주의사항

  • @Transactional설정과 함께 사용할경우 동작이 잘 안된다
  • 순환 참조가 안되게 주의해야한다
  • private method는 사용 불가능하다. Async는 AOP가 적용되어 Bean에 올라가있고 스프링이 Async가 적용된 메서드일경우 해당 메서드를 가로채 다른 스레드에서 작업을 시키기 때문이다
  • self invocation(자가 호출)이 불가능하다. 같은 클래스 내에 존재하거나 자기 자신을 호출할 경우 비동기로 동작하지 않는다
profile
안녕하세요

0개의 댓글