본문 바로가기
Spring

@Async 메일 전송 개선하기

by 희디비 2025. 9. 6.
📌 주의: 해당 글은 잘못된 내용이 포함 되어 있을 수 있습니다.
혹시 잘못된 내용을 발견 하신다면 댓글로 알려주시면 감사 합니다!

 

[ 🌱 회원 가입 ]  

스터디 관리 플랫폼 Wibby 에서는 회원 가입할 때 이메일 인증 후 가입이 가능 합니다.

 

인증 버튼을 누르면 해당 이메일로 인증 번호를 전송 후 인증을 진행 합니다.

 

그런데 인증 버튼을 누르면 3 ~ 4 초 이후에 인증 번호 체크 창이 뜨는 문제가 있습니다.

왜 이런 문제가 발생 하는지 간단한 메일 전송 API를 통해 테스트 해보겠습니다.


[ 🌱 메일 전송 ]  

인증코드를 이메일로 전송 하는 코드 입니다.

@Slf4j
@RequiredArgsConstructor
@Service
public class MailService {

    private final JavaMailSender javaMailSender;
    private final RandomNumberGenerator numberGenerator;
    private final Map<EmailVerification, LocalDateTime> emailVerificationExpirations
    							= new ConcurrentHashMap<>()

    public void sendSimpleMailMessage() {
        SimpleMailMessage simpleMailMessage = new SimpleMailMessage();

        try {
            // 메일을 받을 수신자 설정
            String email = "전송할 이메일";
            simpleMailMessage.setTo(email);

            // 메일의 제목 설정
            simpleMailMessage.setSubject("테스트 메일 제목");

            // 메일의 내용 설정
            String code = stringGenerator.generateRandomString();
            simpleMailMessage.setText(code);

            emailVerificationExpirations.put(
              EmailVerification.of(email, code), LocalDateTime.now().plusMinutes(10)
            );
            javaMailSender.send(simpleMailMessage);

            log.info("메일 발송 성공!");
            log.info("{}", emailVerificationExpirations.get(
              EmailVerification.of(email, code)
            ));
        } catch (Exception e) {
            log.info("메일 발송 실패!");
            throw new RuntimeException(e);
        }
     }

 

메일 전송시 5초 걸리는 이유는 메일 전송이 끝날 때 까지 기다린 후 응답 하였기 때문입니다.

개선 하려면 비동기로 다른 스레드에 작업을 지시 하면 해결 됩니다.

스프링은 Async 어노테이션을 통해 비동기로 작업을 지시할 수 있습니다.


[ 🌱 Executor ] 

어노테이션을 사용 하기 위해서는 먼저 Executor 등록을 해야 합니다.

@Slf4j
@Configuration
public class MailConfiguration {
    
    private static final int CORE_POOL_SIZE = 5;
    private static final int MAX_POOL_SIZE = 5;
    
    @Bean
    public Executor mailExecutor(){
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setQueueCapacity(5);
        executor.setCorePoolSize(5);
        executor.setMaxPoolSize(10);
        executor.setKeepAliveSeconds(60);
        executor.setWaitForTasksToCompleteOnShutdown(false);
        executor.setAwaitTerminationSeconds(0);
        executor.setThreadNamePrefix("MailExecutor-");
        return executor;
    }
}
기능 설명
CorePoolSize 스레드풀 코어 스레드 개수
MaxPoolSize 최대 생성 스레드 개수 ( 큐에 작업이 가득찬 경우 증가 )
KeepAliveSeconds 추가 생성된 스레드 유지 시간
QueueCapacity 작업 큐의 최대 사이즈 ( 기본 = 무한 )
AwaitTerminationSeconds 종료 시 남은 작업을 기다리는 시간 ( 기본 = 0 )
ThreadNamePrefix 생성될 스레드 앞에 붙은 이름
WaitForTasksToCompleteOnShutdown 서버가 종료될 때 큐에 남은 작업을 수행할 것인지 여부

 

코어 스레드 수는 이용자가 적기 때문에 5개로 설정 합니다. ( 기본 )

그래도 혹시나 사용자가 많을 경우를 대비 하여 최대 스레드 개수는 10개로 설정 했습니다. ( 긴급 )

또한 서버가 종료 되면 남은 메일 전송을 보낼 필요는 없기 때문에 남은 작업 수행을 하지 않도록 설정 합니다.


[ 🌱 디버깅 ] 

shutdown 호출 시 waitForTasksToCompleteOnShutdown 설정이 되어 있다면

shutdown을 호출 하고 아닌 경우 shutdownNow를 호출 하여 남은 작업을 수행 하지 않습니다.

 

그리고 AwaitTerminationSeconds 시간 만큼 대기 합니다.


[ 🌱 Async ] 

Async 어노테이션은 스프링 AOP를 사용 하기 때문에 프록시 생성을 위한 빈 후처리기 등록이 필요 합니다.

EnableAsync 어노테이션을 사용 하면 빈 후처리기가 등록 됩니다.

 

이제 비동기로 전송할 메서드에 Async 어노테이션을 붙여 줍니다.

값으로는 빈 이름으로 Executor를 찾기 때문에 등록한 Executor 빈 이름을 작성 합니다.

 

[ 디버깅 ]

AsyncExecutionInterceptor 클래스를 디버깅 하면 메일 전송이 Advice 적용 되었음을 확인 가능 합니다.

 

[ 테스트 ]

 

비동기 작업 수행을 통해 응답 시간을 5초에서 0.1초로 개선할 수 있습니다.

만약 Async 어노테이션 없이 비동기 기능을 만들려면 어떻게 해야 할까요?

 


[ 🌱 MyAsync ] 

작업을 수행할 Executor를 빈으로 등록 합니다.

빈이 제거될 때 shutdown을 호출 하여 작업 호출을 중지 합니다.

@Slf4j
@Component
public class MailExecutor {

    private static final int CORE_POOL_SIZE = 5;
    private static final int MAX_POOL_SIZE = 5;
    private ThreadPoolTaskExecutor executor;

    public MailExecutor() {
        log.info("executor 초기화!");
        this.executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(CORE_POOL_SIZE);
        executor.setMaxPoolSize(MAX_POOL_SIZE);
        executor.setKeepAliveSeconds(0);
        executor.setQueueCapacity(Integer.MAX_VALUE);
        executor.setAwaitTerminationSeconds(0);
        executor.setThreadNamePrefix("MailExecutor-");
        executor.setWaitForTasksToCompleteOnShutdown(false);
        executor.initialize();
    }

    @PreDestroy
    public void close() {
        log.info("executor 종료!");
        executor.shutdown();
    }

    public Executor getExecutorService() {
        return this.executor;
    }
}

 

어노테이션 기반 AOP를 만들기 위해 어노테이션을 생성 합니다.

 

프록시 적용 대상과 부가 기능인 Advisor를 작성 합니다.

어노테이션에 파라미터로 입력한 값을 통해 스프링 컨테이너에서 Executor를 찾고 작업을 수행 합니다.

이제 만든 어노테이션을 사용할 메서드에 적용 하면 됩니다.

 

[ 테스트 ] 

잘 적용 되었습니다!

MyAsync 어노테이션은 Async 기능을 이해 하기 위해 작성을 한 것이므로 부족한 부분이 있을 수 있습니다.

혹시 잘못된 부분이 있다면 알려 주시면 감사 합니다!