프로젝트 진행하면서 회원가입과 비밀번호 찾기 기능에서 이메일 인증을 적용하기로 했다. 처음에는 MySQL을 사용해해서 엔티티를 만들고 해당 엔티티의 상태를 나타내는 필드를 Enum으로 만들어 사용했다. 하지만 팀 내 회의를 통해 만료 기간을 지정할 수 있는 Redis를 사용하기로 결정했다.
이 글에서는 두 가지 방법을 비교할 예정이다. 먼저 엔티티 클래스를 비교할 것이다.
1. Entity 비교
1) MySQL 사용
@Entity
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class EmailCode {
@Id
@Column(name = "email_code_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 25)
private String email;
@Column(nullable = false, length = 7)
private String verifyCode;
@Column(nullable = false)
private EmailCodeStatusEnum emailCodeStatus;
public void changeEmailCodeStatus(EmailCodeStatusEnum emailCodeStatus) {
this.emailCodeStatus = emailCodeStatus;
}
public void changeVerifyCode(String verifyCode) {
this.verifyCode = verifyCode;
}
}
아래 코드는 이메일 코드 상태를 나타내는 Enum 클래스이다. 만약 완료됨 상태라면 이메일로 새로운 코드를 발급받는다. 보류 중이라면 이메일을 확인하라는 메시지가 뜨도록 설계했다. 이에 대한 자세한 코드는 아래에서 확인할 예정이다.
@AllArgsConstructor
@Getter
public enum EmailCodeStatusEnum {
COMPLETED("완료됨"), PENDING("보류중");
private String value;
}
2) Redis 사용
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@RedisHash(value = "emailCode", timeToLive = 210)
public class EmailCode {
@Id
@Indexed
private String email;
private String verifyCode;
}
EmailCodeStatusEnum와 같은 필드가 없어지고 Redis에 적용되는 새로운 EmailCode를 생성했다.
이때 이메일 인증 제한 시간을 3분으로 결정했기 때문에 통신 과정을 고려해 3분 30초로 결정했다.
2. Repository
두 개의 차이는 상속받은 Repository 클래스이다. MySQL을 사용하는 경우 JpaRepository를, Redis는 CrudRepository를 상속받는다.
1) MySQL
public interface EmailCodeRepository extends JpaRepository<EmailCode, Long> {
Optional<EmailCode> findByEmail(String email);
}
2) Redis
public interface EmailCodeRepository extends CrudRepository<EmailCode, Long> {
Optional<EmailCode> findByEmail(String email);
}
3. Service
서비스 코드도 비슷하다. 해당 엔티티를 저장하는 형태가 변경되고 MySQL을 사용할 때는 이메일 코드의 상태를 검사하는 로직이 추가되어 보기에 더 복잡해 보인다.
1) MySQL
@Service
@RequiredArgsConstructor
public class EmailCodeServiceImpl implements EmailCodeService {
private final JavaMailSender javaMailSender;
private final EmailCodeRepository emailCodeRepository;
@Override
@Transactional
public SendEmailCodeRespDto sendEmail(String email) {
EmailCode findEmailCode = emailCodeRepository.findByEmail(email).orElse(null);
String verifyCode = null;
if (findEmailCode != null) {
if (findEmailCode.getEmailCodeStatus() == EmailCodeStatusEnum.PENDING)
throw new CustomApiException("이메일 수신합을 확인하세요");
else if (findEmailCode.getEmailCodeStatus() == EmailCodeStatusEnum.COMPLETED) {
verifyCode = sendVerifyCodeByEmail(email);
findEmailCode.changeEmailCodeStatus(EmailCodeStatusEnum.PENDING);
findEmailCode.changeVerifyCode(verifyCode);
}
}
else {
verifyCode = sendVerifyCodeByEmail(email);
emailCodeRepository.save(EmailCode.builder()
.email(email)
.verifyCode(verifyCode)
.emailCodeStatus(EmailCodeStatusEnum.PENDING) // 보류 중
.build());
}
return new SendEmailCodeRespDto(verifyCode);
}
private String sendVerifyCodeByEmail(String email) {
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
String verifyCode = generateRandomString();
MimeMessageHelper mimeMessageHelper = null;
try {
mimeMessageHelper = new MimeMessageHelper(mimeMessage, false, "UTF-8");
mimeMessageHelper.setTo(email); // 메일 수신자
mimeMessageHelper.setSubject("이메일 인증 코드"); // 메일 제목
mimeMessageHelper.setText("인증 코드를 입력하세요: " + verifyCode); // 메일 본문 내용
javaMailSender.send(mimeMessage);
} catch (MessagingException e) {
throw new CustomApiException("이메일 전송을 실패했습니다");
}
return verifyCode;
}
@Override
@Transactional
public void checkVerifyCode(String email, String verifyCode) {
EmailCode findEmailCode = findEmailCodeByEmailOrElseThrowEx(emailCodeRepository, email);
if (!isEqualsVerifyCode(verifyCode, findEmailCode)) {
throw new CustomApiException("이메일 인증에 실패했습니다");
}
EmailCodeStatusEnum status = findEmailCode.getEmailCodeStatus();
if (status == EmailCodeStatusEnum.COMPLETED) {
throw new CustomApiException("이미 인증된 코드입니다");
} else if (status == EmailCodeStatusEnum.PENDING) {
findEmailCode.changeEmailCodeStatus(EmailCodeStatusEnum.COMPLETED);
}
}
private boolean isEqualsVerifyCode(String verifyCode, EmailCode findEmailCode) {
return findEmailCode.getVerifyCode().equals(verifyCode);
}
// 인증 코드 생성 (7글자)
private static String generateRandomString() {
String characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
StringBuilder randomString = new StringBuilder();
int length = 7;
Random random = new Random();
for (int i = 0; i < length; i++) {
int index = random.nextInt(characters.length());
randomString.append(characters.charAt(index));
}
return randomString.toString();
}
}
코드에서 볼 수 있듯이 예외 처리 과정이 복잡하다.
2) Redis
@Service
@RequiredArgsConstructor
public class EmailCodeServiceImpl implements EmailCodeService {
private final JavaMailSender javaMailSender;
private final EmailCodeRepository emailCodeRepository;
@Override
@Transactional
public SendEmailCodeRespDto sendEmail(String email) {
EmailCode findEmailCode = emailCodeRepository.findByEmail(email).orElse(null);
if (findEmailCode != null)
throw new CustomApiException("이메일 수신함을 확인하세요");
String verifyCode = sendVerifyCodeByEmail(email);
emailCodeRepository.save(EmailCode.builder()
.email(email)
.verifyCode(verifyCode)
.build());
return new SendEmailCodeRespDto(verifyCode);
}
private String sendVerifyCodeByEmail(String email) {
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
String verifyCode = generateRandomString();
MimeMessageHelper mimeMessageHelper = null;
try {
mimeMessageHelper = new MimeMessageHelper(mimeMessage, false, "UTF-8");
mimeMessageHelper.setTo(email); // 메일 수신자
mimeMessageHelper.setSubject("이메일 인증 코드"); // 메일 제목
mimeMessageHelper.setText("인증 코드를 입력하세요: " + verifyCode); // 메일 본문 내용
javaMailSender.send(mimeMessage);
} catch (MessagingException e) {
throw new CustomApiException("이메일 전송을 실패했습니다");
}
return verifyCode;
}
@Override
@Transactional
public void checkVerifyCode(String email, String verifyCode) {
EmailCode findEmailCode = findEmailCodeByEmailOrElseThrowEx(emailCodeRepository, email);
if (!isEqualsVerifyCode(verifyCode, findEmailCode))
throw new CustomApiException("이메일 인증에 실패했습니다");
}
private boolean isEqualsVerifyCode(String verifyCode, EmailCode findEmailCode) {
return findEmailCode.getVerifyCode().equals(verifyCode);
}
// 인증 코드 생성 (7글자)
private static String generateRandomString() {
String characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890";
StringBuilder randomString = new StringBuilder();
int length = 7;
Random random = new Random();
for (int i = 0; i < length; i++) {
int index = random.nextInt(characters.length());
randomString.append(characters.charAt(index));
}
return randomString.toString();
}
}
비교적 코드가 간단해졌다. 이메일 코드의 상태를 확인할 필요가 없으므로 코드가 같은지만 검사하면 된다.
☑️ 추가 문제
Redis사용할 때 Repository를 구현하는 중 에러가 발생했다.
Redis Repository가 Bean으로 등록되지 않는 이슈였다.
Error creating bean with name 'emailCodeRepository' defined in EmailCodeRepository defined in @EnableRedisRepositories declared on RedisRepositoriesRegistrar.EnableRedisRepositoriesConfiguration: Invocation of init method failed; nested exception is org.springframework.data.mapping.MappingException: Entity com.example.login.entity. Token requires to have an explicit id field. Did you forget to provide one using @Id?
Redis에 저장할 이메일 코드 객체의 @Id 어노테이션 import를 잘못해서 발생한 에러였다.
javax.persistence.Id 대신 org.springframework.data.annotation.Id를 import하여 해결했다.
📧 인증 메일 전송
이메일 인증 구현을 성공했다.
댓글