인증 메일 관리
이전 글에서 인증 번호를 담은 메일 발송까지는 구현했다.
이제 인증번호를 DB에 저장하고, 사용자가 입력한 인증번호와 대조하여 인증하는 로직을 작성해야 한다.
인증메일 저장 및 인증 로직 구현에는 아래의 두 가지 방법이 있을것 같다.
- Redis에 저장하고 만료시간 설정을 해둬서 자동으로 오래된 인증 메일이 삭제되게 관리
- 관계형 DB에 저장하고 주기적으로 오래된 인증 메일을 삭제하는 스케줄러 작성
주기적으로 오래된 인증 메일을 삭제하는건 조건 설정하기 귀찮을것 같아서 레디스를 써서 구현해보려 한다.
스케줄러를 사용하는건 다음에 해봐야지.
사용 기술
- Spring Boot 3.2.4 / gradle-kotlin
- Java 17
- SpringDataRedis
- Gmail
1. 사전작업 - Redis 설치
레디스를 쓰려면 우선 EC2든 로컬이든 레디스가 깔려있어야 한다.
인텔리제이 얼티밋 버전은 따로 DB 연결을 관리하는지는 모르겠지만 본인은 흙수저 버전이므로 직접 설치해야 한다.
레디스 설치 및 기본 사용법은 이 글을 참고한다.
2. 스프링부트와 Redis 연동을 위한 설정
2-1. 의존성 추가
스프링 부트에서 레디스를 쓰기 위해 의존성을 추가해줘야한다.
// build.gradle.kts
// 레디스
implementation("org.springframework.boot:spring-boot-starter-data-redis")
2-2. yml 파일 설정
위 의존성을 추가했다면 기본 설정이 돼있어서 yml과 config 설정을 딱히 안해줘도 된다.
그래서 2-2, 2-3은 사실 안해도 되는 설정이긴 하다.
만약 호스트와 포트 번호를 커스텀해서 쓰려면 2-2, 2-3 설정을 하면 된다.
@ConfigurationProperties로 환경 변수 읽어서 설정할거라 당장은 yml의 depth는 딱히 상관없을듯.
// application.yml
spring:
main:
allow-bean-definition-overriding: true // Redis Bean 오버라이딩 허용
data:
redis:
host: localhost // 호스트, localhost = 127.0.0.1 이므로 뭘 입력하든 상관없음
port: 6379 // 포트번호(레디스 기본값이 6379)
2-3. RedisConfig 추가
일단은 RedisTemplate만 쓸거라 생각하기 때문에 아래처럼 설정했다.
다른데서 보면 그냥 RedisConnectionFactory 부분에서
return new LettuceConnectionFactiory(host, port) 만 해도 되던데 난 안됐다.
RedisConnectionFactory를 빈으로 등록해서 써봤는데도 안돼서 connectionFactory.start()를 추가하니까 되더라.
@RequiredArgsConstructor
@ConfigurationProperties(prefix = "spring.data.redis")
public class RedisConfig {
private final String host; // localhost
private final int port; // 6379
@Bean
public RedisTemplate<String, String> redisTemplate() {
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
return redisTemplate;
}
public RedisConnectionFactory redisConnectionFactory() {
LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(host, port);
connectionFactory.start();
return connectionFactory;
}
}
3. MailService 구현
3-1. MailService에서 Redis로 메일 저장
RedisConfig에서 빈 등록한 RedisTemplate를 사용한다.
redisTemplate.opsForValues() 메서드로 ValueOperations를 통해 레디스 DB에 저장한다.
데이터의 만료시간은 3분으로 두고 3분이 지나면 자동 삭제된다.
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class MailVerificationService {
private final MailConfig mailConfig;
private final JavaMailSender mailSender;
private final RedisTemplate<String, String> redisTemplate;
@Transactional
public void sendVerificationCode(String toEmail) throws MessagingException, UnsupportedEncodingException {
String verificationCode = generateVerificationCode();
ValueOperations<String, String> values = redisTemplate.opsForValue();
values.set(toEmail, verificationCode, Duration.ofMinutes(3));
MimeMessage message = createMessage(toEmail, verificationCode);
try {
mailSender.send(message);
} catch (RuntimeException e) {
log.error("[메일 전송 실패]");
throw new MailSendException("메일 전송에 실패했습니다.");
}
}
}
3-2. MailService에서 Redis로 인증번호 확인
ValueOperations를 통해 레디스 DB에 저장된 데이터를 조회한다.
만약 인증메일과 인증번호가 일치하면 만료시간 5분짜리 인증 확인용 데이터를 추가한다.
public void verifyCode(String toEmail, String code) {
ValueOperations<String, String> values = redisTemplate.opsForValue();
String savedVerificationCode = values.get(toEmail);
boolean isVerified = code.equals(savedVerificationCode);
if (isVerified) {
values.set(toEmail, "ok", Duration.ofMinutes(5));
} else {
log.error("[메일 인증 실패]");
throw new InvalidCodeException();
}
}
3-3. 테스트코드 작성
테스트 작성 시 레디스를 Mocking해서 테스트할지, 실제 레디스에 저장해서 테스트할지 고민했다.
2-2, 2-3처럼 따로 Bean 오버라이딩을 하니까 MockBean 주입할 방법을 못찾아서 실제 레디스를 쓰는 테스트를 작성했다.
어차피 각 테스트 후에 DB를 flush하기 때문에 당장은 큰 문제가 없을거라고 생각한다.
아마 2-2, 2-3을 생략하면 MockBean 주입해서 Mocking도 가능할듯.
MailServiceTest
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private MailVerificationService mailVerificationService;
@AfterEach
void tearDown() {
redisTemplate.execute((RedisCallback<Object>) connection -> {
connection.serverCommands().flushDb();
return null;
});
}
@DisplayName("이메일 전송 성공 시 인증 번호가 redis에 저장된다.")
@Test
void sendEmail3() throws MessagingException, UnsupportedEncodingException {
// given
String toEmail = "test@example.com";
MimeMessage mimeMessage = new MimeMessage((Session) null);
when(mailSender.createMimeMessage()).thenReturn(mimeMessage);
// when
mailVerificationService.sendVerificationCode(toEmail);
String result = redisTemplate.opsForValue().get(toEmail);
// then
assertThat(result).isNotNull();
assertThat(result.length()).isEqualTo(6);
}
@DisplayName("올바른 인증번호를 입력하면 redis에 성공 여부를 저장한다.")
@Test
void verifyCode1() {
// given
String email = "test@example.com";
String verificationCode = "123456";
ValueOperations<String, String> values = redisTemplate.opsForValue();
values.set(email, verificationCode, Duration.ofMinutes(3));
// when
mailVerificationService.verifyCode(email, verificationCode);
String result = values.get(email);
// then
assertThat(result).isNotNull();
assertThat(result).isEqualTo("ok");
}
@DisplayName("틀린 인증번호를 입력하면 예외를 던진다.")
@Test
void verifyCode2() {
// given
String email = "test@example.com";
String verificationCode = "123456";
String WrongCode = "111111";
ValueOperations<String, String> values = redisTemplate.opsForValue();
values.set(email, verificationCode, Duration.ofMinutes(3));
// expected
assertThatThrownBy(() -> mailVerificationService.verifyCode(email, WrongCode))
.isInstanceOf(InvalidCodeException.class)
.hasMessage("인증번호를 확인해주세요.");
}
@DisplayName("메일 인증을 요청하지 않은 이메일로 인증 번호를 입력하면 예외를 던진다.")
@Test
void verifyCode3() {
// given
String email = "test@example.com";
String notRequestedEmail = "midcon@nav.com";
String verificationCode = "123456";
ValueOperations<String, String> values = redisTemplate.opsForValue();
values.set(email, verificationCode, Duration.ofMinutes(3));
// expected
assertThatThrownBy(() -> mailVerificationService.verifyCode(notRequestedEmail, verificationCode))
.isInstanceOf(InvalidCodeException.class)
.hasMessage("인증번호를 확인해주세요.");
}
4. 결과 확인
서비스 레이어를 작성했으니 컨트롤러는 원하는대로 작성해서 테스트하면 된다.
인증 메일 발송

인증 메일 발송 후 redis-cli 확인

포스트맨으로 인증번호 입력 후 redis-cli 확인

'API 만들어 보기 > 이메일 인증' 카테고리의 다른 글
이메일 인증 1: Gmail을 이용하여 이메일 발송하기 (0) | 2024.04.19 |
---|