일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- OOP
- TCP/IP
- Java
- Spring Security
- 객체지향
- redis
- docker
- EC2
- 동시성 문제
- N+1 문제
- 네트워크
- AWS
- 알고리즘
- 스프링부트
- OSI 7계층
- 코딩테스트
- JWT
- 3-way-handshake
- 백준
- 성능 개선
- 프로토콜
- 인메모리
- 형상관리
- 로그인
- 트러블 슈팅
- RDS
- JMeter
- Spring
- 4-way-handshake
- Today
- Total
열공스토리
[Spring] 이메일 발신 기능 성능 개선 본문
현재 진행중인 “DevHub” 프로젝트의 회원가입 과정에는 이메일 인증 기능이 존재합니다. 여타 서비스와 같이 자신의 이메일을 입력하고 인증 요청을 하면 입력한 이메일로 인증 코드가 포함된 메일이 발신되고 받은 인증 코드로 이메일 인증을 하게 됩니다.
문제를 인식하게 된 시점은 JMeter 테스트 도구로 이메일 인증 기능을 테스트 하는 과정에서 요청에 대한 응답 시간이 오래 걸리는 걸과를 얻었을 때 였습니다. 이를 계기로 문제를 개선할 필요가 있다고 생각이 들었고 결과적으로 1초 동안 각 요청으로부터 소요된 응답 시간을 평균 4.159초(4159ms) → 0.017초(17ms)로 개선하였습니다.
개선 전 테스트 결과
이메일 인증 기능을 개선하기 전 코드는 다음과 같았습니다.
[서비스 로직]
public MailSendResponse sendEmail(String toEmail){
if (redisUtil.existData(RedisPolicy.MAIL_AUTH_KEY + toEmail)) {
redisUtil.deleteData(RedisPolicy.MAIL_AUTH_KEY + toEmail);
}
try {
MimeMessage emailForm = createEmailForm(toEmail); // 메일 폼 생성
javaMailSender.send(emailForm); // 메일 폼 전송
} catch (MessagingException | UnsupportedEncodingException e) {
throw new MailSendException(ErrorCode.MAIL_SEND_FAILURE);
}
return MailSendResponse.of(toEmail);
}
인증 받을 이메일을 가지고 메일 폼을 만들고 JavaMailSender API로 설정한 SMTP 서버로 메일 폼을 전송합니다. 이러한 상태에서 테스트를 진행해 보겠습니다.
[테스트]
테스트에서 사용한 이메일은 저의 gmail, 네이버 메일 2개와 Temp Mail 사이트에서 제공하는 임시 메일 6개를 사용해서 총 8개의 이메일로 테스트를 진행했습니다.
1. 8개의 각 요청에 대해 쓰레드를 1개씩 생성하고 1초 동안 한 번만 요청을 하도록 설정합니다.
위와 같이 설정 후 서버에 요청을 보내 보겠습니다.
2. 결과
결과는 보다시피, 8개의 요청 모두 성공했습니다. 하지만 각 요청이 응답을 받기까지 평균적으로 4.159초(4159ms)가 소요되었습니다.
3. 문제 인식
이 사진에서는 모든 요청에 대해 응답을 받기까지 “33초”라는 시간이 소요되었다는 사실을 알 수 있습니다. 이 결과를 바탕으로 실제 서비스에서 8명이 이메일 인증 API를 요청했을 경우를 가정한다면 한 유저는 서버로부터 응답이 올 때까지 33초라는 시간 동안 아무 동작도 하지 못하고 대기해야 할 것입니다. 그리고 이러한 사실은 서비스에 매우 치명적인 문제가 됩니다.
원인
각 요청에 대한 응답은 평균적으로 약 4초가 소요됐지만 8개의 요청이 모두 응답을 받는데까지 소요된 시간이 33초였다는 것은 마치 하나의 쓰레드가 요청하고 응답받은 뒤, 그 다음 쓰레드가 요청하고 응답받는 듯한 양상을 보이고 있습니다.
실제로 JavaMailSender의 send() 메서드 실행 전후를 로깅해 봤을 때, “~에게 메일 보내기 전”과 “~에게 메일 보낸 후” 사이에 4초 정도의 텀이 존재했고 모든 요청에 대해 순차적으로 실행되고 있음을 확인했습니다.
이러한 원인은 JavaMailSender가 동기 방식으로 처리되기 때문에 send() 메서드 실행 시점에 SMTP 서버와 통신하는 과정에서 쓰레드가 블로킹되어 하나의 요청이 응답을 받기까지 대략 4초 정도의 시간이 소요되고 여러 요청에 대해 순차적으로 처리되어 모든 요청에 대한 응답을 받기까지 33초라는 시간이 소요된 것입니다.
개선 방법
이 문제를 개선하기 위해서는 동기 방식으로 처리되는 JavaMailSender를 비동기 방식으로 처리되도록 하는 것입니다.
JavaMailSender를 비동기로 처리하기 위해서 다음과 같이 @Async 어노테이션을 메서드 레벨에 명시해줍니다.
@Service
@EnableAsync
@RequiredArgsConstructor
public class AsyncSendService {
private final JavaMailSender javaMailSender;
@Async
public void send(MimeMessage mailForm) throws MessagingException {
javaMailSender.send(mailForm);
}
}
이때, @Async 어노테이션을 사용하기 위해서는 다음의 조건을 충족해야합니다.
- @EnableAsync 어노테이션을 명시하여 @Async가 붙은 메서드가 비동기 처리 되도록 한다.
- @Async는 proxy를 기반으로 동작하는데 self-invocation 즉, 내부 호출에 의해 실행될 경우 proxy의 어드바이스에 의한 추가 작업이 수행되지 않기 때문에 내부 호출에 의해 실행되면 안된다. (*참고 https://velog.io/@qkrmekem/스프링-AOP-프록시와-내부호출)
그리고 다음과 같이 비동기 방식으로 메일 폼을 전송하는 메서드를 호출하도록 기존의 서비스 로직을 바꿔줍니다.
public MailSendResponse sendEmail(String toEmail){
if (redisUtil.existData(RedisPolicy.MAIL_AUTH_KEY + toEmail)) {
redisUtil.deleteData(RedisPolicy.MAIL_AUTH_KEY + toEmail);
}
try {
MimeMessage emailForm = createEmailForm(toEmail); // 메일 폼 생성
asyncSendService.send(mailForm); // 비동기 메일 폼 전송
} catch (MessagingException | UnsupportedEncodingException e) {
throw new MailSendException(ErrorCode.MAIL_SEND_FAILURE);
}
return MailSendResponse.of(toEmail);
}
개선 후 테스트 결과
비동기 방식으로 개선하기 전 테스트와 동일한 환경에서 테스트를 진행했습니다.
1. 결과
사진에서 알 수 있듯이, 각 요청이 응답을 받는데까지 평균 0.017초(17ms)가 소요되었습니다. 이 결과는 동기 방식으로 처리될 때의 소요된 시간보다 4.142초(4142ms)가 감소한 수치입니다.
또한, 모든 요청으로부터 응답을 받기까지 단 “1초”도 걸리지 않은 결과를 얻을 수 있었습니다.
추가적으로 JavaMailSender의 send() 메서드 실행 전후를 로깅해 봤을 때 위와 같이 비동기 방식으로 처리되고 있음을 확인했습니다.
결과적으로 메일 처리 방식을 비동기 방식으로 바꾸고나서 1초 동안 각 요청으로부터 소요된 응답 시간을 평균 4.159초(4159ms) → 0.017초(17ms)로 개선할 수 있었습니다.
+추가
1. 기능 개선 전후 포스트맨 응답 시간
[개선 전]
약 5.15초(5150ms) 소요
[개선 후]
약 0.89초(890ms) 소요
2. 실제 메일 폼
[Gmail]
[Temp Mail]
'Spring' 카테고리의 다른 글
[Spring] 파일 저장 로직의 동시 요청에 의한 동시 저장을 방지하는 방법 (0) | 2024.08.17 |
---|---|
[Spring] 게시판 좋아요 기능 동시성 문제 트러블 슈팅 (2) | 2023.07.28 |
[Spring] N+1 문제 트러블 슈팅 (0) | 2023.07.28 |
[Spring] Redis 사용 해 보기 (0) | 2023.07.28 |
[Spring] Security + JWT 로그인 인증 방식 내부 동작 과정 (0) | 2023.07.28 |