이메일 인증
팀 프로젝트에서 회원가입 기획에서 이메일 인증 기능을 넣기로 했다.
자체 메일 서버를 구축하기 보다는 메일 서버로 Gmail을 이용해서 보내려고 한다.
이번 글에서는 이메일을 발송하기까지만 다루도록 한다.
발송한 이메일을 DB에 저장을 해서 그걸 확인하고 인증하는 과정도 필요하다.
그걸 어떻게 구현할지 아래 두가지 방법 중 고민인데 아직 미정이라서 추후 추가할듯.
- Redis를 써서 시간 설정을 해두고 자동으로 삭제되게 관리
- 관계형 DB를 써서 주기적으로 오래된 인증 메일을 삭제
사용 기술
- Spring Boot 3.2.4 / gradle-kotlin
- Java 17
- Gmail
1. 사전작업 - SMTP 계정 설정
1-1. 구글 계정 관리로 이동
Gmail을 메일 서버로 이용하기 위해 구글 계정의 앱 비밀번호가 필요하다.
구글 로그인 → 구글 계정 관리 → 검색창에 “앱 비밀번호” 검색해서 이동한다.
앱 비밀번호가 안뜨면 2단계 인증을 하지 않았을 수 있으므로 하도록 한다.
1-2. 앱 비밀번호 만들기
원하는 앱 이름을 입력하고 만든다.
1-3. 앱 비밀번호 발급
이 비밀번호를 이용해서 메일을 보낼 예정이니 잘 저장해둔다.
2. JavaMailSender 설정
2-1. 의존성 추가
이메일 전송을 위해 spring-boot-starter-mail의 의존성을 추가한다.
// build.gradle.kts
// 이메일 인증
implementation ("org.springframework.boot:spring-boot-starter-mail")
2-2. yml 파일 설정
이메일 전송을 위한 yml 설정을 추가한다.
본인이 쓰기 편한 dept로 설정한다.
본인은 @ConfigurationProperties를 쓰기 위해 smptProperties depth에 필요 설정을 몰아뒀음.
아래에서 바꿀건 username, password 뿐일듯
// application.yml
gmail:
host: smtp.gmail.com # 구글 이메일 사용 시
port: 587 # 포트번호
username: hyukkind@gmail.com # 구글 이메일, 다른 메일일 시 MailAuthenticationException 발생
password: utnncuqkptaweaag # 앱 비밀번호
smtpProperties:
auth: true
starttls-enable: true
starttls-required: true
connection-timeout: 5000
timeout: 5000
writeTimeout: 5000
2-3. MailConfig 추가
JavaMailSender를 설정하는 방법은 여러가지가 있겠지만
본인은 MailConfig에서 JavaMailSender의 Bean 주입을 해주었다.
yml파일의 환경 변수를 가져오는건 @Value로 하든 @ConfigurationProperties로 하든 취향대로 하자.
기본적인 골자는 JavaMailSender의 property 설정만 아래처럼 Properties 객체로 넣어주면 된다.
@RequiredArgsConstructor
@ConfigurationProperties(prefix = "gmail")
public class MailConfig {
private final String host;
private final int port;
private final String username;
private final String password;
private final SmtpProperties smtpProperties;
public String getFromEmail(){
return username;
}
@Bean
public JavaMailSender javaMailSender() {
JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
mailSender.setHost(host);
mailSender.setPort(port);
mailSender.setUsername(username);
mailSender.setPassword(password);
mailSender.setDefaultEncoding("UTF-8");
mailSender.setJavaMailProperties(getProperties());
return mailSender;
}
private Properties getProperties() {
Properties properties = new Properties();
properties.put("mail.smtp.auth", smtpProperties.auth);
properties.put("mail.smtp.starttls.enable", smtpProperties.starttlsEnable);
properties.put("mail.smtp.starttls.required", smtpProperties.starttlsRequired);
properties.put("mail.smtp.connectiontimeout", smtpProperties.connectionTimeout);
properties.put("mail.smtp.timeout", smtpProperties.timeout);
properties.put("mail.smtp.writetimeout", smtpProperties.writeTimeout);
return properties;
}
@Getter
@ToString
@RequiredArgsConstructor
private static class SmtpProperties {
private final boolean auth;
private final boolean starttlsEnable;
private final boolean starttlsRequired;
private final int connectionTimeout;
private final int timeout;
private final int writeTimeout;
}
}
3. MailService 구현
3-1. MailService 구현
본인은 테스트용 Mock 구현체를 만들 필요가 있지 않을까 싶어서 MailService 인터페이스를 만들었지만
굳이 MailService를 인터페이스로 만들어서 타입 계층을 만들 필요는 없다.
바로 MailService 구현체를 만들어도 무방함.
인증번호는 6자리 랜덤 숫자를 발급하기로 했다.
메일 발송까지만 구현했고, DB에 저장하고 검증하는 로직은 추후 추가 예정이다.
MailVerificationService
@Slf4j
@Service
@RequiredArgsConstructor
public class MailVerificationService implements MailService {
private final MailConfig mailConfig;
private final JavaMailSender mailSender;
@Override
public void sendEmail(String toEmail) throws MessagingException, UnsupportedEncodingException {
MimeMessage message = createEmailVerificationMessage(toEmail);
try {
mailSender.send(message);
} catch (RuntimeException e) {
log.error("[메일 전송 실패]");
throw new MailSendException("메일 전송에 실패했습니다.");
}
}
private MimeMessage createEmailVerificationMessage(String toEmail) throws MessagingException, UnsupportedEncodingException {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message);
helper.setFrom(mailConfig.getFromEmail(), "나는짱");
helper.setTo(toEmail);
helper.setSubject("PAWLAND 이메일 인증");
String template = createTemplate();
helper.setText(template, true);
return message;
}
private String createTemplate() {
String template = "<html><body>";
template += "<p>안녕하세요, PAWLAND입니다.</p>";
template += "<p>인증 번호는 아래와 같습니다:</p>";
template += "<h2>" + generateVerificationCode() + "</h2>";
template += "<p>이 인증 번호를 입력하여 인증을 완료해주세요.</p>";
template += "</body></html>";
return template;
}
private String generateVerificationCode() {
Random random = new Random();
int randomNumber = random.nextInt(999999) + 1;
return String.format("%06d", randomNumber);
}
}
3-2. MailService 테스트
테스트마다 메일이 매번 날아온다면 내 메일함에 테스트용 메일이 가득할 것이라 바람직하지 않다.
그래서 JavaMailSender를 Mocking해서 테스트를 진행한다.
인증번호 생성 로직은 private 메서드라서 어떻게 테스트할까 생각해보았다.
간단한 로직이라 테스트할 필요가 있을까 싶기도 하고 인증번호가 이상하다고 큰 문제가 생기진 않을것 같아서
굳이 추가하지는 않았다.
MailVerificationServiceTest
@SpringBootTest
class MailVerificationServiceTest {
@Autowired
private MailConfig mailConfig;
@MockBean
private JavaMailSender mailSender;
@Autowired
private MailVerificationService mailVerificationService;
@DisplayName("이메일 전송 성공 Mock 테스트")
@Test
void sendEmail1() throws MessagingException, UnsupportedEncodingException {
// given
String toEmail = "test@example.com";
MimeMessage mimeMessage = new MimeMessage((Session) null);
when(mailSender.createMimeMessage()).thenReturn(mimeMessage);
// when
mailVerificationService.sendEmail(toEmail);
// then
verify(mailSender).send(mimeMessage);
verify(mailSender, times(1)).send(mimeMessage);
}
@DisplayName("이메일 전송 실패 Mock 테스트")
@Test
void sendEmail2() {
// given
String toEmail = "test@example.com";
MimeMessage mimeMessage = new MimeMessage((Session) null);
when(mailSender.createMimeMessage()).thenReturn(mimeMessage);
doThrow(new MailAuthenticationException("Authentication failed"))
.when(mailSender)
.send(mimeMessage);
// expected
assertThatThrownBy(() -> mailVerificationService.sendEmail(toEmail))
.isInstanceOf(MailSendException.class)
.hasMessage("메일 전송에 실패했습니다.");
}
}
4. 결과 확인
'API 만들어 보기 > 이메일 인증' 카테고리의 다른 글
이메일 인증 2: Redis로 인증 메일 관리하기 (0) | 2024.04.20 |
---|