본문 바로가기

🧑🏻‍💻 Dev/SpringBoot

[Spring] @Async 이용하여 비동기 구현하기

비동기 구현을 하려는 이유


프로젝트에서 서버에서 발생하는 5xx Exception을 모니터링하기 위해서 Slack WebHook을 사용해서 구현했습니다.

 

현재 로직은 5xx 예외가 발생 -> Slack 메시지 전송 -> 클라이언트에게 응답까지의 과정이 동기적으로 진행됩니다.

 

여기서 사용하고 있는 Slack 서버는 제가 제어할 수 없는 부분이기 때문에 만약 Slack 서버에 지연이 생겨 응답을 늦게 준다면 어떻게 될지 생각을 해봤습니다.

 

Slack 메시지 전송이 지연되어 정작 더 중요한 다음 과정인 "클라이언트에게 응답"이 제대로 수행되지 않는다면... 끔찍한 상황이 벌어질 수도 있다고 생각했습니다.

 

실제로 Slack 메시지를 보내는 push() 메서드에서 5초의 지연 시간을 주고 테스트 해보니 클라이언트에게 응답이 가는 시간도 그만큼 지연이 되는 것을 확인했습니다.

요청이 들어온 시간과 클라이언트에게 응답되는 시간 사이의 5초간의 지연 발생

 

 

그래서 Slack으로 메시지를 보내는 부분을 비동기로 처리하여 클라이언트에게 응답을 제공하는 부분에 지연이 생기지 않도록 구현하기로 했습니다.

 

 

 

🔎 Spring의 @Async


구현을 하기 전에 @Async를 조금 알아보면, 비동기 처리를 간단하게 할 수 있도록 Spring에서 제공해주는 어노테이션입니다.

 

사용 방법은 @Async를 사용하겠다고 @EnableAsyc를 붙여주고, 비동기로 처리할 메서드 위에 @Async를 붙여주면 됩니다.

 

@Async를 사용할 때는 주의 해야할 점이 2가지 있습니다.

  1. private 메서드에 붙여서는 안 됩니다. (컴파일 에러로 확인 가능)
  2. 같은 클래스 내부에서 스스로 호출(self-invocation)을 하면 안 된다. (컴파일 에러로 확인 불가)

 

1번은 private 메서드에 @Async를 붙여보면 IntelliJ에서 컴파일 에러를 확인해 볼 수 있습니다.

private 메서드에 @Async 붙였을 때

 

2번은 아래와 같이 컴파일로 확인이 되지 않습니다.

같은 클래스 내부에 있는 test() 메서드를 self invocation

 

이제 외부에서 MonitoringProvider의 push() 메서드를 호출 했을 때, 내부에 있는 test()가 당연히 비동기 처리 되겠지 하고 생각하고 놔두면 이제 디버깅 지옥을 맛볼 수 있습니다.

 

실제로 외부에서 push() 메서드를 여러 번 요청해서 비동기 처리가 되는지 확인해보면 다음과 같이 순차적으로 동기적으로 처리되는 것을 확인할 수 있습니다.

순차적으로 동기적으로 처리된 모습

 

자가 호출을 하면 동작하지 않는 이유는 Spring 컨테이너에 등록되어 있는 스프링 빈의 메서드가 호출되었을 때, @Async가 붙어있다면 AOP로 처리되어 스프링이 메서드 요청을 가로채서 다른 Thread Pool에서 실행시켜주는 구조이기 때문이라고 합니다.

 

그래서 @Async를 private에 붙이거나 내부 호출을 하게 되면 스프링이 가로채서 프록시 처리를 할 수 없기 때문에 2가지 주의점이 있습니다.

 

 

 

✨ 적용하기


이런 주의점을 확인하고, 기존 로직에 비동기 처리를 진행해줍니다.

 

먼저, @Async를 사용할 수 있도록 @EnableAsync를 붙여줍니다.

@EnableAsync
@SpringBootApplication
public class PraisePushApplication {
    public static void main(String[] args) {
        SpringApplication.run(PraisePushApplication.class, args);
    }
}

 

비동기 처리를 원했던 메시지를 보내는 push() 메서드에 @Async를 붙여줍니다.

public class SlackMonitoringProvider implements MonitoringProvider {

    @Value("${error.webhook.url}")
    private String webHookUrl;

    @Async
    @Override
    public void push(final Exception exception, final HttpServletRequest request) {
        SlackApi slackApi = new SlackApi(webHookUrl);
        slackApi.call(createSlackMessage(exception, request));
    }
    
    ... 생략 ...
}

 

요청 시간 로그를 설정하고, push 로직에서 5초의 지연을 두고 요청을 확인해 보면 메시지를 보내는 로직이 비동기적으로 처리되는 것을 확인할 수 있습니다.

 

Slack으로 메시지 보내는 로직이 비동기로 처리되어 클라이언트에게 응답되는 시간이 먼저 처리되는 것을 확인

 

 

 

🔗 Reference


https://jeong-pro.tistory.com/187

 

How does @Async work? @Async를 지금까지 잘 못 쓰고 있었습니다(@Async 사용할 때 주의해야 할 것, 사용법

@Async in Spring boot 스프링 부트에서 개발자에게 비동기 처리를 손쉽게 할 수 있도록 다양한 방법을 제공하고 있다. 대세는 Reactive stack, CompletableFuture를 쓰겠으나 역시 가장 쉬운 방법으로는 @Async an

jeong-pro.tistory.com

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

 

Effective Advice on Spring Async: Part 1 - DZone

In this post, we explore some of the biggest misconceptions and limitations when working with Spring's Async annotation.

dzone.com