스프링 @Async을 통한 속도 4배 향상 개발 사례
안녕하세요. 인시퀀스 개발팀 입니다. 이번 포스팅에서는 작년 인시퀀스 홈페이지 견적문의 고도화를 하면서 발생했던 이슈와 해결 과정을 소개하고자 합니다.
견적문의 접수가 느려요
작년 인시퀀스 홈페이지 견적문의 고도화 과정에서 견적문의 상담 접수 시 등록 완료 알림을 받기까지 시간이 너무 오래 걸린다는 의견이 제기되었습니다. 견적문의는 인시퀀스 홈페이지에서 고객과의 첫 소통을 시작하는 중요한 기능이기에 개선이 필요했습니다.

// 제목 설정 message.setSubject(text, "UTF-8"); // 내용 설정 message.setText(content, "UTF-8","html"); // 메일 송신 Transport.send(message);
분석해보니 견적문의 접수시 담당자에게 접수알림 이메일이 발송되는데 Transport.send(message)
를 사용한 메일서버에 연결 및 송신하는 과정에서 지연시간이 발생되고 동기적으로 처리되면서 사용자는 완료 알림을 받기까지 기다려야 하는 문제가 있었습니다.

동기처리시 왼쪽 그림처럼 Task1이 Task2를 호출하면 Task2의 작업이 종료될 때까지 대기해야하지만 비동기처리시 Task2의 작업종료여부에 상관없이 Task1은 기존에 수행하던 작업을 이어서 진행할 수 있습니다.
이를 개선하기 위해 스프링에서 제공하는 @Async
을 사용해서 메일 발송을 비동기적으로 처리하도록 시도했습니다.
@Async을 사용한 비동기 메서드 적용방법
1. 설정 클래스 작성
@EnableAsync @Configuration public class MailAsyncExecutor implements AsyncConfigurer { @Override public Executor getAsyncExecutor() { .... } @Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { .... } }
먼저 메서드 비동기처리를 위해 AsyncConfigurer인터페이스를 구현하는 설정 클래스를 작성했습니다.
@Configuration
을 추가해서 설정 클래스임을 명시했습니다.@Async
활성화를 위해@EnableAsync
을 추가했습니다.AsyncConfigurer
인터페이스를 구현하였고getAsyncExecutor()
와getAsyncUncaughtExceptionHandler()
를 Override합니다.
AsyncConfigurer
인터페이스를 구현하지않거나 Executor
타입의 인스턴스를 빈으로 등록하지 않고 기본 설정만을 사용한다면 SimpleAsyncTaskExecutor
가 설정됩니다. SimpleAsyncTaskExecutor
에 대한 내용은 뒤에서 설명드리겠습니다.
2. Thread Pool 설정
@Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setMaxPoolSize(10); executor.setCorePoolSize(5); executor.setQueueCapacity(50); executor.setThreadNamePrefix("mailExecutor"); executor.initialize(); return executor; }
getAsyncExecutor()
는 비동기처리를 위한 Executor
타입의 인스턴스를 반환하고 Thread Pool을 생성합니다. 보편적으로 ThreadPoolTaskExecutor
을 사용해서 Thread Pool을 설정합니다.
setMaxPoolSize(10)
: 최대 가용되는 쓰레드 수를 10개로 설정합니다.
setCorePoolSize(5)
: 초기 쓰레드 수를 5개로 초기화 합니다.
setQueueCapacity(50)
: 쓰레드 풀 큐의 사이즈 입니다. CorePoolSize의 개수를 넘는 작업이 들어오는 경우 큐에 task가 쌓이게 됩니다.
setThreadNamePrefix(”mailExecutor”)
: 생성되는 쓰레드 명의 접두사를 설정합니다.
initialize()
: 스레드 풀을 초기화합니다. 설정된 값들을 바탕으로 스레드 풀을 구성합니다.
3. 예외 처리
@Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return (throwable, method, objects) -> { logger.error("Message : {}", throwable.getMessage()); logger.error("비동기호출 실패 메서드 명: {}",method.getName()); Stream.of(objects).forEach(param -> { logger.error("파라미터 : {}", String.valueOf(param)); }); }; }
getAsyncUncaughtExceptionHandler()
는 비동기 처리 도중 발생한 예외를 처리할 수 있는 AsyncUncaughtExceptionHandler
를 반환하고 handleUncaughtException()
를 제공합니다.
3가지의 파라미터를 받습니다.
throwable
: 비동기 메서드에서 발생한 예외
method
: 예외가 발생한 비동기 메소드 정보
objects
: 예외가 발생한 메소드의 파라미터 정보
@Async을 사용한 비동기 메서드 적용결과
1. 실행 로그 확인
-
첫번째 실행 결과
-
두번째 실행 결과
적용 결과 로그를 확인해보면 Thread명이 다른 것을 확인할 수 있고 비동기 메서드를 처리하는 Thread명이 Thread Pool설정 과정에서 작성했던 ThreadNamePrefix와 일치하는 것을 확인할 수 있습니다.
그리고 비동기 처리로 메인작업이 먼저 끝나고 비동기 메서드 작업이 수행되는 경우도 있어 완전히 다른 Thread로 처리되는 것을 확인할 수 있습니다.
2. 예외 발생시 로그 확인

getAsyncUncaughtExceptionHandler()
에 작성했던 로그 내용이 출력된 것을 확인할 수 있습니다.주의해야 돼요
1. self-invocation
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Import(AsyncConfigurationSelector.class) public @interface EnableAsync { .... AdviceMode mode() default AdviceMode.PROXY; ..... }
@Async
을 사용한 비동기 메서드는 기본적으로 스프링 AOP에 의해 프록시 패턴으로 실행됩니다. @EnableAsync
의 기본 모드는 프록시 모드 입니다. 비동기 처리될 메서드가 실행되기 전 프록시 객체가 앞단에서 먼저 호출되고 프록시 객체에 의해 비동기 처리될 메서드가 호출됩니다.
@Async public void mailSend(){ //메일발송 로직 ... .. } //클라이언트 객체에 의해 실행되는 메서드 public void saveEstimate() { //메일발송 메서드 self-invocation this.mailSend(); .. . }
따라서 위와 같이 객체 내부에서 자기 자신의 메서드를 호출하는 자가호출(self-invocation)은 비동기 메서드 처리가 실행되지 않습니다.
2. ThreadPool

Executor Bean을 설정하지 않으면 SimpleAsyncTaskExecutor
을 기본적으로 사용하게 됩니다. Spring공식 문서에서 SimpleAsyncTaskExecutor
에 대한 내용을 찾아보면 “이 구현은 스레드를 재사용하지 않고 호출할 때마다 새 스레드를 시작합니다.” 라는 내용이 있는데 이 방법은 ThreadPooling방식이 아닌 새로운 Thread를 생성해 작업을 처리함을 의미합니다.
로그를 확인해보면 작업을 수행할 때마다 새로운 Thread가 생성되는 것을 볼 수 있습니다. 새로운 Thread를 생성하는 방법은 서버에 부담을 주는 작업입니다.
따라서 ThreadPooling 방식을 지원하는 ThreadPoolTaskExecutor
를 사용해서 비동기 메서드 작업을 진행하며 현재 상황과 서버 자원에 맞게 적절하게 설정하는 것이 필요합니다.
마치며
비동기 메서드 처리를 통해 4초 정도 걸렸던 견적 접수 대기 시간을 1초 미만으로 단축 시켰습니다.
@Async
를 사용해 현재 메인작업에 영향을 주지 않으면서 백그라운드로 처리하는 기법은 이메일발송 이외에도 다양한 상황에서 활용이 가능할 것입니다.
프로젝트마다 상황에 가장 적합한 처리 방법이 무엇인지 고민해보고 찾아가는 과정이 가장 중요하다는 사실을 깨닫게 되는 경험이었습니다.
안녕하세요, 인시퀀스 개발팀입니다. 신속하고 안정적인 솔루션을 구축하여 최고의 사용자 경험을 제공하며, 지속적인 개선을 통해 만족스러운 서비스를 제공하겠습니다.