Spring/Issue

SMTP 비동기로 보내기(@Async)

블로그 주인장 2024. 1. 9.

ISSUE

회원가입을 할 때 이메일을 통해서 인증을 하고 있다. 하지만 SMTP는 외부서비스이며 실제로 굉장히 느리다.

HTTP 요청을 보내고 완료까지 시간이 오래된다면, 이메일 인증을 하는 유저 입장에서는 인증받는데 딜레이가 걸리게 된다.

 

기존 내용

코드 내용은 다음과 같다.

 

현재 코드는 인증코드를 랜덤으로 작성해서 MimeMessageHelper를 이용하여

코드 정보와 메일 내용을 작성한 후에 JavaMailSender를 이용해서 메일을 발송한다.

 

아래 있는 redis의 경우에는 이메일 정보를 이메일의 만료시간까지 저장하고 있다가 유효성 검증 시에 사용한다.

 

HTTP 요청을 받으면 요청 시 Dto로 인증번호와 해당 이메일을 반환받도록 설정이 되어있다.

 

 

이메일을 인증하는데 14초 가 걸리는 문제가 있기에 이를 해결할 수 있도록 하려고한다.

필자의 경우 14초가 걸렸지만, PC 나 노트북 사양에 따라 평균 2~3 초 정도로 소요되는 것으로 보인다.

 

 

비동기 프로그래밍

비동기 프로그래밍은 대용량 데이터 처리, 느린 I/O 작업, 복잡한 계산 등

다양한 작업을 병렬로 처리하면서 시스템의 응답 시간을 개선하고, 리소스를 효율적으로 사용하는데 도움이 된다.

스프링에서는 @Async 어노테이션을 통해 이러한 비동기 메서드를 간단하게 실행할 수 있다.

 

Exception Handling

기본적으로 @Async 메서드에서 발생하는 예외는 호출자에게 전파가 되지 않는다.

이는 @Async 어노테이션이 붙은 메서드가 별도의 스레드에서 실행되므로 메인 스레드에서 캐치할 수 없기 때문이다.

 

예외를 처리하기 위해서는 AsyncUncaughtExceptionHandler 를 사용하여 예외를 적절히 처리해야한다.

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new MyAsyncUncaughtExceptionHandler();
    }
}

 

위와 같이 AsyncUncaughtExceptionHandler 인터페이스로 구현해서 getAsyncUncaughtExceptionHandler를 오버라이딩하여 사용자가 정한 예외 핸들러를 사용할 수 있습니다.

 

비동기 설정 파일 작성

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {

  @Bean(name = "mailExecutor")
  public Executor asyncMailExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(2);
    executor.setMaxPoolSize(5);
    executor.setQueueCapacity(10);
    executor.setThreadNamePrefix("MailExecutor");
    executor.initialize();
    return executor;
  }
}

 

Spring에서는 @EnableAsync 옵션만 추가해도 비동기 처리를 사용할 수 있다.

 

이를 커스터마이징하기 위해서는 위와 같이 설정파일을 만들어준다.

 

- CorePoolSize : 기본적으로 실행 대기중인 thread 개수(기본값 : 1)

- MaxPoolSize : 동시에 동작하는 최대 thread 개수(기본값 : Integer.Max_VALUE)

- QueueCapacity : CorePool의 크기를 넘어설 때 저장되는 큐의 최대 용량 (기본값 : Integer.Max_VALUE)

 

기본적으로 Spring은 SimpleAsyncTaskExecutor 를 사용하여 메서드를 비동기적으로 실행한다.

 

SimpleAsyncTaskExecutor 는 요청이 오는대로 계속해서 thread를 생성한다.

 

만약, 이때 Executor 타입의 Bean이 한개라면 해당 Bean으로 작동한다.

하지만, 여러 개의 빈으로 등록해야한다면 SimpleAsyncTaskExecutor가 돌아가므로 이름을 통해 명시해주어야 한다.

 

아래 예시처럼 @Bean에 이름을 설정해주면 된다.

@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean(name = "threadPoolTaskExecutor")
    public Executor threadPoolTaskExecutor() {
        //...
    }

    @Bean(name = "anotherExecutor")
    public Executor anotherExecutor() {
        //...
    }
}

 

 

변경 내용

 

이전 코드에서 @Async 어노테이션 하나만 붙여주면 된다.

 

이렇게 되면 한 스레드에서는 성공 응답값을 보내주고 다른 스레드에서는 이메일을 전송하고 있을 것이다.

 

@Async 어노테이션이 붙은 메서드는 void, Future, CompletableFuture 이 중 하나의 반환 타입을 가져야한다.

 

만약 Future를 반환하는 경우, 비동기 작업의 결과를 반환하고, 해당 작업의 완료를 대기할 수 있게 됩니다.

 

현재 필자의 프로젝트의 경우, 이메일에 대한 인증 반환값이 필요없기 때문에 void 형식으로 변환한 상태이다.

 

 

이를 보면 14.44s -> 160ms 약 111% 감소된 것으로 확인할 수 있다.

 

마무리

비동기 프로그래밍은 시스템의 성능을 향상시키는 데에는 매우 유용하지만, 주의해야할 점들이 많습니다.

특히 스프링에서 @Async 어노테이션을 사용하여 비동기 메서드를 만들 때에는 Exception Handling, Configuration 등 적절히 고려해서 작동해야한다는 것을 알게되었습니다.

 

 

Reference

https://www.baeldung.com/spring-async

 

https://medium.com/@choyun0415/spring-smtp%EC%99%80-%EB%B9%84%EB%8F%99%EA%B8%B0-b567125433e3

 

 

본 포스트는 작성자가 공부한 내용을 바탕으로 작성한 글입니다.
잘못된 내용이 있을 시 언제든 댓글로 피드백 부탁드리겠습니다.
항상 정확한 내용을 포스팅하도록 노력하겠습니다.

반응형

'Spring > Issue' 카테고리의 다른 글

Stomp를 활용한 웹소켓 구현  (1) 2024.01.24
JPA Persistable 로 엔티티 구별하기  (0) 2024.01.08
Scheduler Thread Pool 사용하기  (1) 2024.01.08

댓글