Notice
Recent Posts
Recent Comments
Link
«   2024/09   »
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
Tags
more
Archives
Today
Total
관리 메뉴

DevYGwan

Spring Mail 인증 비동기 처리 & 비동기 retry 정책 적용 본문

Study/Spring

Spring Mail 인증 비동기 처리 & 비동기 retry 정책 적용

YGwan 2023. 12. 17. 15:58

 

 

 

 프로젝트 내에서 유저가 대학생인지 확인하기 위해서 유저 대학생 이메일을 인증하는 로직을 적용하려 했습니다. 이 로직을 적용하기 위해서 실제 대학생 이메일에 인증번호를 보내고 이 인증번호를 확인하는 로직을 구현하였습니다.

 구현에 사용한 기술을 간단히 설명하면

 

  • Spring Mail
  • Google SMTP
  • 인증 코드 생성 & Redis를 통한 인증
  • 비동기 처리
  • 비동기 에러 처리 정책 (retry & 로깅)

 

 이런 기술을 사용하여 구현하였습니다. 이를 간단히 도식화하면

 

 

 위의 그림과 같습니다. 이제부터 사용된 기술과 이를 어떻게 적용했는지를 설명하도록 하겠습니다. Google SMTP를 위한 계정 설정은 이에 대한 자료가 많으니 따로 설명을 하진 않겠습니다. Google SMTP를 적용하려면 구글 계정 설정을 추가로 해줘야합니다. (ex 2단계 인증, 구글 메일 설정 등)

 물론 구글이 아닌 다른 플랫폼에서 제공하는 SMTP 서비스를 사용해도 됩니다. 하지만 그 플랫폼에서 처리해야하는 추가적인 설정은 각각 존재하니 이에 대한 처리는 필수적입니다.

 

 


 

 

※  Spring Mail

 Spring Mail은 Spring Framework의 일부로 제공되는 기능 중 하나입니다. 이는 Java Mail API를 기반으로 구축된 Spring의 메일 전송 및 수신 기능을 제공합니다. 즉 이메일 관련 작업 (이메일 전송, 템플릿, 수신 등)을 더욱 편리하게 처리할 수 있도록 Spring에서 제공하는 기능입니다. 이 기능을 사용하려면 아래의 의존성 추가를 필수적으로 해야합니다.

 

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-mail'
}

 

 이 의존성을 추가하면 JavaMailSender 사용할 수 있습니다. 이를 의존성 주입으로 주입받고 이 메서드에서 제공하는 send 메서드를 통해 제가 원하는 메일을 보낼 수 있습니다. 이메일 템플릿의 경우 기본적으로 Spring Mail에서 제공하는 SimpleMailMessage 객체를 사용하여 구현할 수 있습니다. 해당 객체를 생성하고 set 메서드를 통해 값을 설정하고 최종적으로 JavaMailSender에서 제공하는 send 메서드의 매개변수에 넣으면 원하는 메일 형태에 맞는 메일을 보낼 수 있습니다.

 간단한 예시 코드를 살펴보자면,

 

@Service
public class MailService {

  @Autowired
  private JavaMailSender mailSender;
    
  public MailService(JavaMailSender mailSender) {
    this.mailSender = mailSender;
  }

  public void send(String toAddr, String text, String fromAddr) {
	final SimpleMailMessage message = new SimpleMailMessage(); 
	message.setFrom(fromAddr);
	message.setTo(toAddr); 
	message.setText(text);
	mailSender.send(message);
  }
}

 

이런 방식으로 메일 서비스를 구현할 수 있습니다.

 

 

 

※  SMTP

 SMTP는 Simple Mail Transfer Protocol의 약자로, 인터넷 상에서 이메일을 전송하기 위해 사용되는 표준 프로토콜입니다. SMTP는 이메일 클라이언트(송신자)가 이메일 서버(수신자)로 이메일을 전송하는 데 사용됩니다. 따라서 메일을 전송할때 Spring Mail과 함께 사용됩니다. Google SMTP를 사용하기 위해서는 이에 대한 설정 파일이 필요합니다. 설정 파일에 들어갈 내용은 다음과 같습니다.

 

spring:
  mail:
    host: ${SMTP_HOST}
    port: ${SMTP_PORT}
    username: ${SMTP_USERNAME}
    password: ${SMTP_PASSWORD}
    properties:
      mail:
        smtp:
          auth: true # 사용자 인증 시도 여부 (기본값 : false)
          timeout: 5000 # Socket Read Timeout 시간(ms) (기본값 : 무한대)
          starttls:
            enable: true # StartTLS 활성화 여부 (기본값 : false)
  • SMTP_HOST는 smtp 서버 호스트 값을 의미합니다. ( Google SMTP : smtp.gmail.com )
  • SMTP_PORT는 smtp에서 사용하는 포트 번호를 의미합니다. ( 대부분 587 포트를 사용합니다. )
  • SMTP_USERNAME는 보낸 사람에 해당하는 아이디를 의미합니다.
  • SMTP_PASSWORD는 위에서 계정 설정에서 생성된 앱 비밀번호를 의미합니다.

 

 이  파일은 조심해야합니다. 구글 이메일과 앱 비밀번호가 있어 노출된다면 다른 사람들도 제가 설정한 구글 이메일을 통해 메일 서비스를 사용할 수 있기 때문입니다. 따라서 이 파일을 gitignore하거나 spring env 변수로 설정하여 환경 변수로 설정해 노출하지 않도록 하는 것이 필수적입니다. ( 저는 후자의 방법으로 처리했습니다.)

 

 

 


※  이메일 인증 방식

 이렇게 Spring Mail과 SMTP를 사용해서 메일을 보낼 수 있는 환경까지는 구현이 완료가 되었습니다. 그렇다면 이제 이메일 인증을 어떤식으로 처리했는지 설명하도록 하겠습니다. 인증 처리를 위해서는 Redis를 사용했습니다.

순서는 다음과 같습니다.

  1. 랜덤한 값의 인증 코드 생성
  2. 인증 코드를 redis에 저장
  3. 인증 코드를 메일 전송
  4. 인증 코드 입력
  5. redis에 접근하여 해당 인증코드가 맞는지 확인

 

인증  코드 생성 시 서버 내부 로직

  • 사용자는 인증코드를 통한 메일을 요청 보내는 작업만 할 뿐 실제 서버 내부에서 메일을 보내는 로직을 기다릴 필요가 없습니다.
  • 따라서 사용자는 랜덤 코드 생성 & 레디스에 저장 & 메일 요청의 작업 후에 응답을 받고 그 이후에 서버가 비동기적으로 SMTP 서버에서의 응답을 기다립니다.
  • 이렇게 함으로써, 사용자는 더 빠른 응답 시간을 기대할 수 있습니다.

 

 

인증  코드 인증 처리시 서버 내부 로직

  • 사용자가 인증 코드를 입력하면, redis에서 해당 이메일에 맞는 인증코드를 조회합니다.
  • 조회 후에 입력한 인증코드와 조회된 인증코드를 비교하여 맞으면 success 응답을 틀리면 Failed 응답을 보냅니다.
  • 인증 유효시간은 최대 3분이며, 3분이 지난 이후에는 재요청을 보내야합니다. 이는 redis의 ttl설정을 통해 구현하였습니다.

 

레디스를 사용한 이유

1. 레디스는 인메모리 데이터 저장소로서 데이터를 메모리에 저장하므로 매우 빠른 읽기 및 쓰기 작업이 가능합니다.
2. ttl설정을 통해서 데이터 처리가 수월합니다.
3. 서버가 다운되어 DB 내용이 삭제되더라도 주요한 데이터가 아니기 때문에 성능 향상이 더 효율적입니다. (쉽게 복구 가능)

 

 

 

※  비동기 적용

 

 사용자가 메일 인증 요청을 보낼때 실제로 서버에서 메일을 보내는 것이 아닙니다. 서버는 단순히 SMTP 서버에 메일 요청을 보내는 것이고 실제 메일은 SMTP 서버에서 보냅니다. 이때, 사용자가 요청을 보내고 SMTP 서버가 메일을 보내는 것까지 확인하고 응답을 받는다면 바로 응답을 받을 수가 없습니다. 그만큼 '지연'이 생기는 것입니다.

 만약 사용자가 메일 요청을 보내고 응답이 오는데 시간이 오래걸린다면 사용자 경험이 좋지 못할 것입니다. 또한 사용자는 단순히 메일 요청을 보내는 것이지 그 메일을 보내는 것은 서버가 할 일입니다. 따라서 이를 비동기적으로 처리해 메일을 전송하는 것은 다른 스레드로 처리하도록 로직을 구현했습니다. 이를 구현하기 위해서는 @Async 어노테이션과 @EnableAsync 어노테이션을 사용하여 구현할 수 있습니다.

 

- @Async

@Async는 스프링 프레임워크에서 비동기적인 메서드 실행을 지원하기 위한 어노테이션입니다. 이 어노테이션은 메서드나 클래스에 적용할 수 있고 해당 로직이 비동기적으로 실행됩니다. 이 로직은 Spring의 AOP를 통해 구현됩니다.
 일반적으로 메서드는 호출되면 호출한 쓰레드에서 순차적으로 실행되지만, @Async를 사용하면 호출한 쓰레드가 작업을 호출한 후에 다른 쓰레드에게 작업을 위임하고 바로 리턴됩니다. 이렇게 함으로써 작업을 비동기적으로 실행하여 호출한 쓰레드가 다른 작업을 수행하거나 대기하지 않고 동시에 여러 작업을 처리할 수 있게 됩니다.

 구현은 아래와 같이 할 수 있습니다.

@Service
public class MailService {

  @Value("${mail.fromAddr}")
  private String fromAddr;
  
  @Autowired
  private JavaMailSender mailSender;
    
  public MailService(JavaMailSender mailSender) {
    this.mailSender = mailSender;
  }
  
  @Async
  public void sendMail(String userAddr, MailForm form) {
    sendMail(userAddr, form, fromAddr);
  }
}

 

 

이렇게 구현하면 sendMail 메서드가 실행될때 비동기적으로 처리됩니다. 이때 사용되는 스레드 풀을 관리하기 위해서 Executor를 직접 설정할 수도 있습니다. Executor란 자바에서 스레드를 관리하고 실행하기 위한 인터페이스입니다. 이를 구현한 코드를 살펴보자면,

 

@Configuration
@EnableAsync
public class AsyncConfig {

  @Bean
  public Executor taskExecutor() {
 	final ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
	executor.setCorePoolSize(corePoolSize);
	executor.setMaxPoolSize(maxPoolSize);
	executor.setQueueCapacity(queueCapacity);
	executor.initialize();
	executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
	return executor;
  }
}
  • CorePoolSize : 최소한으로 유지해야 하는 스레드의 수
  • MaxPoolSize : 스레드 풀의 최대 갯수, 동시에 동작할 수 있는 최대 쓰레드 수
  • QueueCapacity : 작업을 대기시킬 수 있는 작업 큐의 용량 수

 

이렇게 스레드 관련 설정을 할 수 있습니다. 또한 @EnableAsync 어노테이션을 사용하여 비동기 기능을 활성화시켜야 합니다. 이 어노테이션은 main 메서드가 있는 클래스에 추가할 수도 있습니다.

 

 

 

 

이렇듯 비동기 적용 전과 후의 사용자가 응답 받는 시간이 확연히 차이가 나는 것을 확인할 수 있습니다.



 

최종 전송 메일 이미지

 

 

 

※  비동기 retry 정책 적용

 이렇게 비동기로 메일 서비스를 구현하는 방법을 소개했습니다. 이렇게 하면 비동기 처리가 완료가 됩니다. 그런데 이 로직에는 문제가 있습니다. 정상적으로 메일 요청 -> 메일 전송 로직이 동작하면 문제가 없지만 smtp 서버가 문제가 생겨 메일 전송 로직이 정상적으로 동작하지 않으면 문제가 발생합니다.

 예를 들어, Google smtp 서버가 잠시 꺼져 메일 전송이 안된다고 가정했을때 만약 사용자가 메일 요청을 보내면 서버는 사용자에게 200 응답을 보내고 smtp 서버에 메일 전송 요청을 추가적인 스레드에서 보낼 것입니다. 하지만 실제 사용자에게는 200 응답이 갔지만 smtp서버에 문제가 생겨 실제로는 메일이 전송이 안되는 문제가 발생합니다.

 

이러한 문제를 해결하기 위해 저는 재시도 정책으로 로직을 구현했습니다. 문제가 생기면 최대 3번까지 추가적인 요청을 보냅니다. 총 3번의 재시도 끝에도 문제가 발생하면 서버 에러를 발생시키고 이를 서버 로그에 남겨두는 식으로 구현했습니다.

 

 

- 구현 방식

@Async
public void sendMail(String userAddr, MailForm form) {
  sendMail(userAddr, form, fromAddr);
}

public void sendMail(String userAddr, MailForm form, String fromAddr) {
  final SimpleMailMessage mailMessage = new SimpleMailMessage();
  mailMessage.setTo(userAddr);
  mailMessage.setSubject(form.title());
  mailMessage.setText(form.body());
  mailMessage.setFrom(fromAddr);

  final int retryCount = 3;
  sendMail(retryCount, mailMessage);
}

public void sendMail(int retryCount, SimpleMailMessage mailMessage) {
  if (retryCount < 0) {
    throw new TingServerException(MSER_005);
  }

  try {
    mailSender.send(mailMessage);
  } catch (Exception e) {
    sendMail(retryCount - 1, mailMessage);
  }
 }

 

 

- 영상

보시는 바와 같이 문제가 생겻을 경우 3번의 재시도를 비동기적으로 진행하고 최종적으로 문제가 발생하면 에러를 발생시킵니다. 이 에러는 GlobalExceptionHandler에 의해 서버 로그로 남겨집니다.