- 클라이언트가 이메일 인증 코드를 요청한다.
- 서버는 이메일 중복 여부를 확인한 뒤, 이메일 인증코드를 보내준다.
- 클라이언트는 해당 이메일로 수신된 이메일 인증코드로 검증을 요청한다.
- 서버는 해당 인증코드를 검증하여 결과를 전송한다.
결국 서버에서는 이메일 인증코드를 생성하여 클라이언트에게 보내준 뒤,
그 이메일 인증코드를 보관하고 있다가, 클라이언트가 인증코드 확인을 요청하면 그 코드가 올바른 코드인지를 확인할 수 있어야 한다.
여기서는 그 이메일 인증코드를 인메모리 데이터 저장소인 Redis에 저장한다.



그럼 요로코롬 생성 완료
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
spring boot 3.0이후로 spring.redis 가 spring.data.redis로 변경되었다.
# redis
spring:
data:
redis:
port: 8081
expireMinutes: 3
host: localhost
@Configuration
public class RedisConfig {
@Value("${spring.data.redis.host}")
private String host;
@Value("${spring.data.redis.port}")
private int port;
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(host,port);
}
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory());
return redisTemplate;
}
}
implementation 'org.springframework.boot:spring-boot-starter-mail'
spring:
mail:
host: smtp.naver.com
port: 465
username: 네이버 아이디
password: 패스워드
from: 네이버 아이디@naver.com
properties:
mail:
smtp:
auth: true
ssl:
enable: true
trust: smtp.naver.com
📌 이 때, 만일 2단계 인증이 되어있는 네이버 아이디를 사용할 경우,
password에 실제 네이버 비밀번호가 아닌 따로 생성되는 어플리케이션 비밀번호를 입력해주어야 한다.위의 2단계 인증을 클릭해서 들어가면 어플리케이션 비밀번호를 생성하는 기능이 있다.
@Configuration
public class MailConfig {
@Value("${spring.mail.host}")
private String host;
@Value("${spring.mail.from}")
private String fromAddress;
@Value("${spring.mail.password}")
private String password;
@Value("${spring.mail.port}")
private int port;
@Bean
public JavaMailSender javaMailService() {
JavaMailSenderImpl javaMailSender = new JavaMailSenderImpl();
javaMailSender.setHost(host);
javaMailSender.setUsername(fromAddress);
javaMailSender.setPassword(password);
javaMailSender.setPort(port);
javaMailSender.setJavaMailProperties(getMailProperties());
return javaMailSender;
}
private Properties getMailProperties() {
Properties properties = new Properties();
properties.setProperty("mail.transport.protocol", "smtp");
properties.setProperty("mail.smtp.auth", "true");
properties.setProperty("mail.smtp.starttls.enable", "true");
properties.setProperty("mail.debug", "true");
properties.setProperty("mail.smtp.ssl.trust","smtp.naver.com");
properties.setProperty("mail.smtp.ssl.enable","true");
return properties;
}
}
@RequiredArgsConstructor
@Component
public class RedisUtil {
private final StringRedisTemplate redisTemplate;
public String getData(String key) {
ValueOperations<String,String> valueOperations = redisTemplate.opsForValue();
return valueOperations.get(key);
}
public void setData(String key, String value) {
ValueOperations<String,String> valueOperations = redisTemplate.opsForValue();
valueOperations.set(key,value);
}
public void setDataExpire(String key, String value, long durationMin) {
ValueOperations<String,String> valueOperations = redisTemplate.opsForValue();
Duration expireDuration = Duration.ofMinutes(durationMin);
valueOperations.set(key,value,expireDuration);
}
public void deleteData(String key) {
redisTemplate.delete(key);
}
}
일단 기본적으로 데이터를 가져오고, 저장하고, 삭제하는 기능만 구현하였다.
@RequiredArgsConstructor
@Service
public class MailService {
private final JavaMailSender javaMailSender;
private final RedisUtil redisUtil;
@Value("${spring.data.redis.expireMinutes}")
private long expireMin;
@Value("${spring.mail.from}")
private String fromAddress;
public void sendMail(String email, MimeMessage message) throws Exception{
try{
javaMailSender.send(message);
} catch(MailException mailException) {
mailException.printStackTrace();
throw new IllegalAccessException();
}
}
@Transactional
public void sendVerificationEmail(String email) throws Exception {
String code = createRandomCode(6);
MimeMessage mimeMessage = createVerificationMessage(email,code);
sendMail(email,mimeMessage);
redisUtil.setDataExpire(code,email,expireMin);
}
private MimeMessage createVerificationMessage(String email, String code) throws Exception{
MimeMessage message = javaMailSender.createMimeMessage();
message.addRecipients(Message.RecipientType.TO,email);
message.setSubject("Lettrip 이메일 인증 코드입니다.");
message.setText(createVerificationEmailText(code),"utf-8","html");
message.setFrom(new InternetAddress(fromAddress,"Lettrip"));
return message;
}
private String createVerificationEmailText(String code) {
String text = "";
text += "아래 코드를 Lettrip 이메일 인증 코드란에 입력해주세요.";
text += "<br>";
text += "CODE : <strong>";
text += code;
text += "</strong>";
return text;
}
@Transactional
public void verifyEmailCode(String code) {
String email = redisUtil.getData(code);
if(email==null) {
throw new LettripException(LettripErrorCode.EMAIL_CODE_NOT_MATCH);
}
redisUtil.deleteData(code);
}
public String createRandomCode(int length) {
return UUID.randomUUID().toString().substring(0,length);
}
}
실질적 이메일 전송을 맡는 클래스이다
@Service
@RequiredArgsConstructor
public class AuthService {
private final UserRepository userRepository;
@Transactional
public SignUpUser.Response createUser(SignUpUser.Request request) {
checkIfDuplicatedEmail(request.getEmail());
return SignUpUser.Response.fromEntity(createUserFromRequest(request));
}
private User createUserFromRequest(SignUpUser.Request request) {
return userRepository.save(User.builder()
.email(request.getEmail())
.password(request.getPassword())
.name(request.getName())
.nickname(request.getNickname())
.imageUrl(request.getImageUrl())
.providerType(ProviderType.LOCAL)
.build());
}
public void checkIfDuplicatedEmail(String email) {
userRepository.findByEmail(email)
.ifPresent(user -> {
throw new LettripException(LettripErrorCode.DUPLICATED_EMAIL);
});
}
}
이메일이 존재하는 지 확인하는 checkIfDuplicatedEmail 메서드를 아예 void로 빼서 공통적으로 사용할 수 있도록 하였다.
@RequiredArgsConstructor
@RestController
@RequestMapping("/api")
public class AuthController {
private final AuthService authService;
private final MailService mailService;
...
@GetMapping("/email-code/{email}")
public ApiResponse sendEmailVerificationCode(@PathVariable String email) throws Exception {
authService.checkIfDuplicatedEmail(email);
mailService.sendVerificationEmail(email);
return new ApiResponse(true, "이메일 인증 코드가 메일로 전송되었습니다.");
}
@GetMapping("/email-verify/{code}")
public ApiResponse verifyEmailCode(@PathVariable String code) {
mailService.verifyEmailCode(code);
return new ApiResponse(true, "이메일 인증이 완료되었습니다.");
}
}
실행되는 모든 메서드들의 로직에서 오류가 발생할 경우엔 오류 메세지가 응답으로 전송되고, 에러 없이 성공적으로 모든 로직이 성공된다면 ApiResponse에 확인 메세지가 담겨 전달된다.
ApiResponse.java
@Getter @Setter @RequiredArgsConstructor public class ApiResponse { private boolean success; private String message; public ApiResponse(boolean success,String message) { this.success = success; this.message = message; } }



성공적으로 검증 되는 것을 볼 수 있다.

test@naver.com의 이메일로 가입한 회원이 있는 상태에서 이메일 인증 요청 시

DUPLICATED_EMAIL 에러가 잘 발생하는 것을 확인!

EMAIL_CODE_NOT_MATCH 에러가 잘 발생하는 것을 확인 !