제가 개발중인 대학교 네트워킹 플랫폼은 신분 인증을 위해 이메일 인증이 필수적인 요소입니다. 본 포스팅에서는 Spring Boot를 활용하여 이메일 인증 시스템을 구현, 레디스를 활용하여 인증시간 단축과 API 호출회수 제한, 비동기화를 도입하여 사용자 경험 개선한 사례를 설명드리겠습니다.다.
주요 목표
- 이메일 인증 번호 발송 시스템 설계 및 구현
- Redis를 활용한 인증 번호 관리
- API 호출 횟수 제한을 통해 시스템 안정성 강화
인증 이메일을 보낼 이메일(Google, Naver ...)마다 다르겠지만 추가적인 이메일 설정이 필요합니다. 저는 Gmail을 사용했습니다. Gmail 설정은 이 링크를 참고하세요!
Gradle
이메일 전송을 위해 Spring Boot Starter Mail 의존성을 추가합니다.
implementation'org.springframework.boot:spring-boot-starter-mail'
1. 기본 구현
Mailconfig
JavaMailSender를 설정합니다. Gmail SMTP 서버를 사용하며, 이메일 전송 시 SSL/TLS를 활성화합니다.
@Configuration
public class MailConfig {
@Value("${email.id}")
private String fromId;
@Value("${email.password}")
private String password;
@Bean
public JavaMailSender getJavaMailSender() {
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
mailSender.setHost("smtp.gmail.com");
mailSender.setPort(465);
mailSender.setUsername(fromId);
mailSender.setPassword(password);
Properties props = mailSender.getJavaMailProperties();
props.put("mail.transport.protocol", "smtp");
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.ssl.enable", "true");
props.put("mail.debug", "true");
return mailSender;
}
}
- @Value로 설정되어 있는 값은 yml에 설정하시면 됩니다
- smtp 프로트콜 이용 (465 포트번호)
- JavaMailSender 객체를 생성하여. 이 객체를 사용해서 이메일을 보내는 것입니다.
RandomGenerator
@Component
public class RandomGenerator {
private static final Random random = new Random();
public int makeRandomNumber() {
// 난수의 범위 111111 ~ 999999 (6자리 난수)
return random.nextInt(888888) + 111111;
}
}
SendMailService
@Service
@RequiredArgsConstructor
public class SendMailService {
private final JavaMailSender mailSender;
@Value("${email.id}")
private String fromId;
@Async("mailExecutor")
public void sendEmail(String to, String subject, String content) {
MimeMessagePreparator messagePreparator =
mimeMessage -> {
//true는 멀티파트 메세지를 사용하겠다는 의미
final MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "UTF-8");
helper.setFrom(fromId);
helper.setTo(to);
helper.setSubject(subject);
helper.setText(content, true);
};
mailSender.send(messagePreparator);
}
}
2. 레디스를 이용한 인증번호 저장
이메일 인증에서는 인증번호를 저장하고 검증하는 과정이 필수적입니다. 이를 위해 관계형 데이터베이스 대신 **레디스(Redis)**를 사용했습니다. 레디스는 인메모리 데이터베이스로, 인증번호와 같은 단기 데이터를 저장하고 빠르게 조회하는 데 적합합니다.
왜 레디스를 사용했는가?
- 속도: 인증번호 검증은 빈번히 발생하며 빠른 처리가 요구됩니다. 레디스는 관계형 데이터베이스보다 빠르게 처리할 수 있습니다.
- TTL(Time-To-Live): 레디스를 사용하면 인증번호의 유효기간을 설정할 수 있어, 만료된 데이터를 자동으로 제거할 수 있습니다.
- 기존 사용 환경: 이미 프로젝트에서 레디스를 사용하고 있었기 때문에 자연스럽게 레디스를 활용했습니다.
인증번호 저장 코드
@Service
@RequiredArgsConstructor
public class AuthMailService {
private static final Long EXPIRATION = 1800000L; // 30분
private final RedisUtil redisUtil;
private final RandomGenerator randomGenerator;
private final SendMailService sendMailService;
@Transactional
public AuthNumberResponse sendCodeEmail(String email, Long key) {
int authNumber = randomGenerator.makeRandomNumber();
String title = "회원 가입 인증 이메일입니다.";
String content = "인증 번호는 " + authNumber + "입니다.";
// 이메일 전송
sendMailService.sendEmail(email, title, content);
// 레디스에 인증번호 저장
redisUtil.saveAuthNumber(key, String.valueOf(authNumber), EXPIRATION);
return new AuthNumberResponse(authNumber);
}
}
RedisUtil 클래스 예시
@Component
@RequiredArgsConstructor
public class RedisUtil {
private final RedisTemplate<String, String> redisTemplate;
//이메일 emailAuthNumber 관련 redis 명령어
public void saveAuthNumber(Long key, String emailAuthNumber, Long expiration) {
redisTemplate.opsForValue().set("AuthNumber"+String.valueOf(key), emailAuthNumber, expiration, TimeUnit.MILLISECONDS);
}
public Object findEmailAuthNumberByKey(Long key) {
return redisTemplate.opsForValue().get("AuthNumber"+String.valueOf(key));
}
}
3. 비동기화로 사용자 경험 향상
비동기 처리의 필요성
- 이슈: 이메일 전송에 평균적으로 5초가 소요되어 클라이언트의 요청 응답이 지연됨.
- 해결: 비동기 처리로 메인 스레드의 작업을 방해하지 않고, 전송 작업을 별도 스레드에서 수행.
구현 방법
Spring의 @Async를 활용하여 비동기 처리했습니다. 별도의 스레드풀을 구성해 성능을 최적화했습니다.
비동기 메일 전송 코드
@Service
@RequiredArgsConstructor
public class SendMailService {
private final JavaMailSender mailSender;
@Async("mailExecutor")
public void sendEmail(String to, String subject, String content) {
MimeMessagePreparator messagePreparator = mimeMessage -> {
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "UTF-8");
helper.setTo(to);
helper.setSubject(subject);
helper.setText(content, true);
};
mailSender.send(messagePreparator);
}
}
스레드풀 구성
@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;
}
}
4. API 호출 횟수 제한으로 안정성 강화
이메일 인증 API는 사용자 요청에 따라 호출되며, 과도한 호출은 서버에 부하를 줄 수 있습니다. 이를 방지하기 위해 레디스를 활용해 호출 횟수를 제한했습니다.
호출 제한 설계
- 제한 조건: 30분 동안 최대 10회 호출 가능.
- 구현 방식: 사용자의 고유 키(예: Provider ID)를 기준으로 호출 횟수를 저장하고 관리.
호출 제한 코드
@Service
@RequiredArgsConstructor
public class RateLimitService {
private static final int MAX_API_CALL = 10;
private static final Long EXPIRATION = 1800000L; // 30분
private final RedisUtil redisUtil;
public boolean checkAPICall(Long key) {
Object apiCall = redisUtil.findAPICallByKey(key);
if (apiCall == null) {
redisUtil.saveAPICall(key, "1", EXPIRATION);
return true;
} else if (Integer.parseInt((String) apiCall) < MAX_API_CALL) {
redisUtil.updateAPICall(key, String.valueOf(Integer.parseInt((String) apiCall) + 1));
return true;
}
return false;
}
}
호출 제한 적용
@Transactional
public AuthNumberResponse sendCodeEmail(String email, Long key) {
if (rateLimitService.checkAPICall(key)) {
return sendAuthCode(email, key);
}
throw new CustomException(HttpStatus.TOO_MANY_REQUESTS, "Too many requests.");
}
5. 결론
비동기화 처리로 응답시간이 5s -> 20ms 로 250배로 성능향상을 하였고, 시스템의 안정성을 위해 redis를 통해 호출 횟수 제한하였습니다.
결과
6. 이메일 인증 API 향후 개선 방향
- 스케일링을 고려한 Redis 클러스터링 도입
- 현재 단일 Redis를 활용해 인증번호와 호출 제한을 관리하고 있으나, 서비스 확장 시 Redis 클러스터링을 통해 데이터 처리 성능과 안정성을 높일 필요가 있습니다.
- 대용량 트래픽을 위한 기술 체인지
- 간단한 구현을 위해 Gmail SMTP를 사용하였으나, 서비스 유저가 많아질시 대용량 트래픽을 감당하기 위해 Amazon SES와 같은 클라우드 서비스를 고려해볼 수도 있습니다.
- 사용자 요청 증가 대비 큐 시스템 적용
- 이메일 발송 요청이 폭주할 경우를 대비해 RabbitMQ 또는 Kafka와 같은 큐 시스템을 도입.
- 모니터링 및 알림 시스템 구축
- 이메일 발송 실패율, 인증번호 입력 실패 횟수 등을 모니터링하여 문제가 발생할 경우 즉각적인 알림을 받을 수 있도록 APM(Application Performance Monitoring) 시스템 도입.
- 예: New Relic, Datadog, ELK Stack.
- 이메일 발송 실패율, 인증번호 입력 실패 횟수 등을 모니터링하여 문제가 발생할 경우 즉각적인 알림을 받을 수 있도록 APM(Application Performance Monitoring) 시스템 도입.
'스프링' 카테고리의 다른 글
@NoArgsConstructor(access = AccessLevel.PROTECTED) 쓰는 이유 (0) | 2024.04.06 |
---|---|
Intellij에서 gradle 버전이 안 보일 때 해결법 (0) | 2024.04.06 |
mysql, gradle 종속성 관리 변경된 점 (0) | 2024.01.31 |
[인가 설정 리팩토링] Security Config -> @PreAuthorize (0) | 2024.01.25 |
JWT Token Service 개발 중 고민, Tip (0) | 2024.01.24 |