💡 스프링 이메일 인증을 구현할 때에, 인증번호에 대한 값을 하나의 변수에 넣고 관리를 하도록 코드를 짰었다. 그러나 만약 여러 유저에게 이메일 인증 요청이 들어온다면? 마지막에 요청한 유저만이 이메일 인증 번호 매칭을 성공할 수 있다는 큰 문제점이 발생한다. 그래서 이에 대한 문제 해결을 지금부터 해보고자 한다. 앞선 이메일 인증 기능에 대한 코드는 https://velog.io/@mk020/Spring-%EC%9D%B4%EB%A9%94%EC%9D%BC-%EC%9D%B8%EC%A6%9D-%EA%B8%B0%EB%8A%A5-%EA%B5%AC%ED%98%84%ED%95%98%EA%B8%B0 내가 작성한 해당 글에서 확인할 수 있다.
💡 더불어, 이메일 인증 요청시 꽤 오랜 시간이 걸리는데, 지연 시간을 줄이고자 @Async
를 통해 비동기 처리 해주었다.
수정된 부분에 대해서만 작성하겠다.
✔️ 먼저, 각 사용자의 인증번호를 저장하는 해시맵을 만들어줬다. 메일을 보내고 나서, mail과 number를 맵에 저장한다. 이를 통해 여러 유저가 동시에 인증번호를 요청하여도 모두 매칭확인이 가능하다.
✔️ 비동기 처리를 위하여 sendMail 메서드에 @Async
애노테이션을 붙여주었다. 원래 sendMail 메서드는 int를 반환하는 메서드였으나, 비동기 처리를 위해서는 void
로 바꿔주어야 한다.
@Service
@RequiredArgsConstructor
public class MailService {
private final JavaMailSender javaMailSender;
private static final String senderEmail= "메일을 보낼 구글 이메일";
// 각 사용자의 인증 번호를 저장하는 맵
private final Map<String, Integer> emailVerificationMap = new HashMap<>();
@Async
public void sendMail(String mail) {
int number = createNumber();
MimeMessage message = javaMailSender.createMimeMessage();
try {
message.setFrom(senderEmail);
message.setRecipients(MimeMessage.RecipientType.TO, mail);
message.setSubject("이메일 인증");
String body = "";
body += "<h3>" + "요청하신 인증 번호입니다." + "</h3>";
body += "<h1>" + number + "</h1>";
body += "<h3>" + "감사합니다." + "</h3>";
message.setText(body,"UTF-8", "html");
javaMailSender.send(message);
//사용자 이메일과 인증 번호 매핑 저장
emailVerificationMap.put(mail, number);
} catch (MessagingException e) {
e.printStackTrace();
}
}
public int getVerificationNumber(String mail) {
return emailVerificationMap.getOrDefault(mail, -1); // 해당 이메일의 인증 번호 반환, 없으면 -1 반환
}
private int createNumber() {
return (int)(Math.random() * (90000)) + 100000; //(int) Math.random() * (최댓값-최소값+1) + 최소값
}
public boolean checkVerificationNumber(String mail, int userNumber) {
int storedNumber = getVerificationNumber(mail);
return storedNumber == userNumber;
}
}
그리고 getVerificationNumber
메서드와 checkVerificationNumber
메서드를 추가하여, 이메일을 통해 유저별로 이메일 인증번호를 매칭, 확인시킬 수 있도록 코드를 작성해주었다.
(이메일 전송 부분은 코드가 거의 같아서 생략)
private final MailService mailService;
@GetMapping("/mailCheck")
public ResponseEntity<?> mailCheck(@RequestParam String mail, @RequestParam int userNumber) {
boolean isMatch = mailService.checkVerificationNumber(mail, userNumber);
return ResponseEntity.ok(isMatch);
}
✔️ mailService
의 checkVerificationNumber
메서드를 통해 사용자의 이메일과 입력한 인증번호를 매칭시켜 올바른 인증번호를 입력하였는지 확인한다. 맞다면 true
, 다르면 false
를 반환한다.
💡 포스트맨을 통해 두 개의 계정에 이메일 인증 메일을 보내고 인증번호 확인을 해본 결과, 두 개 모두 true
값을 반환하는 성공적인 결과값이 나왔다. 시행착오 끝에 드디어 원하는 결과가 나왔을 때의 쾌감이란😁
비동기 처리 전, 이메일을 보내는 데 걸리는 시간은 무려 3.8s였다. 이 지연 시간을 줄이고자 비동기 처리를 해주었다.
앞서 잠깐 언급했지만, 비동기적으로 실행하려는 sendMail
메서드에 @Async
애노테이션을 붙여주기만 하면 된다. (코드는 위 mailService
참고)
그전에, 비동기 처리에 대한 Configuration을 만들어 주어야 하며, @EnableAsync
애노테이션을 붙여주어야 한다.
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
@Bean(name = "mailExecutor")
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(10);
executor.setThreadNamePrefix("Async MailExecutor-");
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return AsyncConfigurer.super.getAsyncUncaughtExceptionHandler();
}
⭐ 또한, 비동기 처리를 위해서는 SpringBootApplication
에 @EnableAsync
애노테이션을 추가해 주어야 한다.
💡 비동기 처리 결과, 이메일 전송을 하는 데 걸리는 시간을 13ms로 줄일 수 있었다. 이렇게나 시간을 많이 단축시킬 수 있다니.. 전과 비교해보면 정말 빠른 속도이다! 비동기 처리를 통해, 이메일 전송 작업이 메인 애플리케이션 스레드를 차단하지 않고 백그라운드에서 실행되기 때문이다. 이를 통해 애플리케이션의 응답성과 성능을 향상시킬 수 있었다.
참고
https://velog.io/@injoon2019/%EC%9D%B4%EB%A9%94%EC%9D%BC-%EB%B9%84%EB%8F%99%EA%B8%B0%EB%A1%9C-%EB%B3%B4%EB%82%B4%EA%B8%B0-Async
https://conact12.tistory.com/entry/Spring-Boot-%EB%B9%84%EB%8F%99%EA%B8%B0-%EB%A9%94%EC%9D%BC-%EC%A0%84%EC%86%A1-%EA%B5%AC%ED%98%84-%EA%B5%AC%EA%B8%80-%EB%A9%94%EC%9D%BC
감사합니다. 저번 글에 이어서 잘보고 가네요. 그런데 또 의문점이 있습니다!
1. HashMap을 사용하면 동시성 문제가 발생해서 ConcurrentHashMap을 사용한다고 하던데, 맞는지 궁금합니다!
2. 계속해서 Map에 인증번호를 담게 되면 메모리 낭비가 발생할거 같은데, 만료기간을 따로 구현하는게 좋지 않을까 생각합니다!