IT-CAST 프로젝트에서 선택된 데이터를 정해진 시간에 메일로 보내는 로직이 있습니다.
@Service
@RequiredArgsConstructor
public class MailService {
private final AmazonSimpleEmailService amazonSimpleEmailService;
private final EmailSender emailSender;
public void send(final SendMailRequest sendMailRequest) {
try {
final SendEmailRequest emailRequest = emailSender.from(sendMailRequest);
amazonSimpleEmailService.sendEmail(emailRequest);
} catch (Exception ex) {
throw new AmazonClientException(ex.getMessage(), ex);
}
}
}
현재 사용 유저는 2명이고 스케줄러가 돌아가는 시간은 646ms입니다.
이 중 2명한테 메일 보내는 로직은 518ms으로 약 80%나 차지하고 있습니다.
여기서 문제점은 다음과 같이 2가지가 있습니다.
- 로그를 찍어본 결과 한 요청에 대해서 할당된 스레드가 로직을 전부 담당하는 점
- 사용자가 많아지면 메일 전송 시간이 계속 늘어나며, 그로 인해 스케줄링 시간이 매우 길어지게 되는 점
비동기 이메일 처리 선택지
1. 병렬 처리
속도는 빠를 수 있지만, 예를 들어 100명의 유저에게 메일을 보낸다고 가정했을 때 100개의 스레드를 생성해야 합니다. 이로 인해 리소스 낭비가 발생하고 시스템의 성능에 부담을 줄 수 있어, 병렬 처리 방식만으로는 적합하지 않다고 판단하였습니다.
2. 스레드 풀 (Thread Pool)
스레드 풀을 사용하면 스레드를 미리 생성하고 재사용함으로써 시스템 자원을 더 효율적으로 관리할 수 있습니다. 스레드 수를 제한할 수 있어 보다 더 효율적인 비동기 처리가 가능합니다.
3. @Async 사용
Spring에서 제공하는 @Async 어노테이션을 활용하면 비동기 작업을 손쉽게 처리할 수 있습니다. 기본적으로 SimpleAsyncTaskExecutor가 사용되지만, 이를 재정의하여 ThreadPoolTaskExecutor를 지정함으로써 스레드 풀을 효율적으로 사용할 수 있습니다.
선택한 방법: 스레드 풀 + @Async
2번과 3번 방법을 결합한 방식을 선택했습니다. @Async를 통해 비동기 작업을 간편하게 처리하고, 스레드 풀을 설정하여 리소스를 좀 더 효율적이게 사용하는 방법을 채택했습니다.
비동기로 메일 전송하기
현재 문제점은 하나의 스레드가 가장 오래 걸리는 메일 전송까지 담당하기 때문에 전체적인 성능이 저하됩니다.
@EnableAsync + @Async
@SpringBootApplication
@EnableScheduling
@EnableAsync
public class ScheduleApplication {
public static void main(String[] args) {
SpringApplication.run(ScheduleApplication.class, args);
}
}
@Service
@RequiredArgsConstructor
public class MailService {
private final AmazonSimpleEmailService amazonSimpleEmailService;
private final EmailSender emailSender;
@Async
public void send(final SendMailRequest sendMailRequest) {
try {
final SendEmailRequest emailRequest = emailSender.from(sendMailRequest);
amazonSimpleEmailService.sendEmail(emailRequest);
} catch (Exception ex) {
throw new AmazonClientException(ex.getMessage(), ex);
}
}
}
실행 클래스에 @EnableAsync를 붙여주거나 따로 config 파일에 붙여주면 되고, 비동기 처리를 원하는 로직에 @Async를 붙여주면 됩니다.
메일 전송 로직을 별도의 스레드한테 위임하고 비동기로 돌아가게 함으로써
기존 동기 처리 : 646ms
비동기 처리 : 42ms로 약 93% 향상된 모습을 볼 수 있습니다.
SimpleAsyncTaskExecutor
위와 같이 @EnableAsync + @Async만 붙여주면 모든 게 해결될까? 그렇지 않습니다.
@EnableAsync는 기본적으로
- TaskExecutor 빈을 찾거나 taskExecutor라는 이름을 가진 빈을 찾는다.
- 둘 다 못 찾으면 SimpleAsyncTaskExecutor를 활용한다.
그리고 SimpleAsyncTaskExecutor는
- 각 Task 마다 새로운 스레드를 생성하고 비동기적으로 실행한다.
- 스레드를 재사용하지 않는다.
예를 들어, 이메일을 각 사용자에게 보내는 작업을 할 때, 사용자가 많아질수록 각각의 메일 전송마다 새로운 스레드가 생성되며, 그로 인해 스레드 생성과 소멸에 대한 리소스 낭비가 심해집니다.
이를 위해 TaskExecutor 빈을 찾거나, taskExecutor라는 이름을 가진 빈을 찾아야 하므로 별도로 TaskExecutor 빈을 정의하고, 이를 관리하는 ThreadPool을 설정해야 합니다.
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "taskExecutor")
public Executor mailTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2); // 기본적으로 사용할 최소 스레드 수
executor.setMaxPoolSize(3); // 최대 스레드 수, 이 수 이상으로는 스레드를 더 생성하지 않음
executor.setQueueCapacity(50); // 대기할 수 있는 작업 수
executor.setThreadNamePrefix("MailTaskExecutor-");
executor.initialize();
return executor;
}
}
@Async("taskExecutor")
public void send(final SendMailRequest sendMailRequest) {
try {
final SendEmailRequest emailRequest = emailSender.from(sendMailRequest);
amazonSimpleEmailService.sendEmail(emailRequest);
} catch (Exception ex) {
throw new AmazonClientException(ex.getMessage(), ex);
}
}
위와 같은 방식으로 taskExecutor라는 이름을 가진 ThreadPoolTaskExecutor 빈을 등록한 후, @Async("taskExecutor")를 붙이면 Spring은 기본적으로 제공되는 SimpleAsyncTaskExecutor 대신 이 설정한 ThreadPoolTaskExecutor를 사용합니다.
다음과 같이 MailTaskExecutor라는 스레드로 메일 발송이 진행되는 걸 확인할 수 있습니다.
'WEB' 카테고리의 다른 글
Index와 Redis를 통해 조회 속도 개선하기 (2) | 2024.11.20 |
---|---|
Spring Security를 사용해 JWT 인증하기 (1) | 2024.11.18 |
[JPA] N+1 문제와 프록시 강제 초기화 해결 (0) | 2024.10.23 |
[WebClient] WebClient를 사용한 날씨 API 리팩토링 (0) | 2024.10.15 |
[JPA] JPA Update 실패 해결기 (0) | 2024.10.10 |
소프트 딜리트란? (1) | 2024.09.27 |
Spring Data JPA로 된 코드를 JDBC로 다시 짜보기 (1) | 2024.09.25 |
MDC를 이용해 세부 로그 확인하기 (0) | 2024.09.23 |