[Spring] 이메일 인증 기능 : 동시에 여러 유저에게 이메일 인증 요청이 들어온다면? +@Async 비동기 처리

Miin·2023년 9월 25일
0

Spring

목록 보기
4/17
post-custom-banner

💡 스프링 이메일 인증을 구현할 때에, 인증번호에 대한 값을 하나의 변수에 넣고 관리를 하도록 코드를 짰었다. 그러나 만약 여러 유저에게 이메일 인증 요청이 들어온다면? 마지막에 요청한 유저만이 이메일 인증 번호 매칭을 성공할 수 있다는 큰 문제점이 발생한다. 그래서 이에 대한 문제 해결을 지금부터 해보고자 한다. 앞선 이메일 인증 기능에 대한 코드는 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를 통해 비동기 처리 해주었다.

이메일 인증

mailService

수정된 부분에 대해서만 작성하겠다.

✔️ 먼저, 각 사용자의 인증번호를 저장하는 해시맵을 만들어줬다. 메일을 보내고 나서, 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 메서드를 추가하여, 이메일을 통해 유저별로 이메일 인증번호를 매칭, 확인시킬 수 있도록 코드를 작성해주었다.


MailController

(이메일 전송 부분은 코드가 거의 같아서 생략)

  • 이메일 인증번호 확인
	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);
    }

✔️ mailServicecheckVerificationNumber 메서드를 통해 사용자의 이메일과 입력한 인증번호를 매칭시켜 올바른 인증번호를 입력하였는지 확인한다. 맞다면 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();
    }
  • setCorePoolSize(2): 기본적으로 실행 대기 중인 Thread 의 개수
  • setMaxPoolSize(10): 동시에 동작하는 최대 Thread의 개수
  • setQueueCapacity(500): CorePool의 크기를 넘어서면 큐에 저장하는데 그 큐의 최대 용량

⭐ 또한, 비동기 처리를 위해서는 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

profile
컴퓨터공학전공 학부생 Back-end Developer
post-custom-banner

1개의 댓글

comment-user-thumbnail
2024년 8월 29일

감사합니다. 저번 글에 이어서 잘보고 가네요. 그런데 또 의문점이 있습니다!
1. HashMap을 사용하면 동시성 문제가 발생해서 ConcurrentHashMap을 사용한다고 하던데, 맞는지 궁금합니다!
2. 계속해서 Map에 인증번호를 담게 되면 메모리 낭비가 발생할거 같은데, 만료기간을 따로 구현하는게 좋지 않을까 생각합니다!

답글 달기