- TIL -
구현 목표)
오늘은 프론트로부터 이메일 정보를 넘겨받아 백에서 랜덤한 회원가입 코드를 생성한 뒤 전달 받은 회원의 이메일로 회원가입 코드를 전송하는 코드를
작성해보려 한다.
보통은 이메일 인증을 redis 와 같은 인메모리 DB 로 구현하지만 나는 사용 중인 주 DB 인 MySQL 을 사용하여 이메일 인증 정보를 저장하고 사용하고자 하였다.
코드의 흐름은 간단하게 아래와 같이 구현하고자 했다.
1. 사용자가 회원가입시 자신의 이메일을 작성한 뒤 인증번호 받기 버튼을 클릭
2. 프론트에서 작성된 이메일 정보를 백엔드에게 전달
3. 백엔드에서 이메일 정보를 전달 받은뒤 회원가입된 이메일인지 아닌지 확인
( 이미 가입된 회원이라면 상태코드 409 반환 )
4. 한 이메일을 기준으로 5분에 3번까지만 인증 시도를 허용할 것이므로 DB 를 시간 기준으로 출력하여 5분 내에 회원가입 시도가 5회 이상인지 확인
( 5회 이상으로 시도한 회원이라면 상태코드 429 반환 )
5. 가입되지 않은 이메일 정보라면 랜덤 회원가입 코드를 생성하여 DB 에 이메일 정보와 인증 시간 값을 저장한 뒤 회원가입 코드를 전송
위 과정 중 오늘 구현할 부분은 회원가입 시도 횟수 체크 부분인 4번을 제외한 모든 부분을 구현해 볼 것이다.
구현 코드)
1. Controller
package com.randomchat.main.controller.register;
import com.randomchat.main.dto.register.EmailVerificationDTO;
import com.randomchat.main.service.email.EmailVerificationService;
import com.randomchat.main.service.register.RegisterService;
import jakarta.mail.MessagingException;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
@RequiredArgsConstructor
@RestController
@RequestMapping("/register")
public class EmailVerificationController {
private final EmailVerificationService emailVerificationService;
private final RegisterService registerService;
@PostMapping("/email/verification")
public ResponseEntity<String> emailVerification(@RequestBody EmailVerificationDTO emailVerificationDTO) throws MessagingException, IOException {
String email = emailVerificationDTO.getEmail();
// 1. 이메일 인증이 들어온 이메일 정보로 가입된 계정이 있다면 deny
if(registerService.checkEmailDuplication(email)) return ResponseEntity.status(409).body("이미 가입된 이메일입니다.");
// 2. 이메일 인증이 들어온 기준 시간으로 부터 5분 내로 DB 내에 5회 인증 요청이 있다면 deny
// emailVerificationService.esExceededLimit(email);
// 3. 이메일 인증용 난수 생성
String verificationCode = emailVerificationService.createCode();
// 4. 이메일을 발송한 뒤 이메일 인증 데이터 DB 에 저장
emailVerificationService.sendEmail(email, verificationCode); // 인증 이메일 발송
emailVerificationService.saveEmailVerification(email, verificationCode); // DB 에 인증 내용 저장
return ResponseEntity.ok("이메일이 발송되었습니다.");
}
}
2. RegisterService ( JPA 쿼리 메소드를 사용하여 동일한 메일이 있는지 체크 )
package com.randomchat.main.service.register;
import com.randomchat.main.domain.users.Users;
import com.randomchat.main.dto.register.RegisterDTO;
import com.randomchat.main.repository.UsersRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class RegisterService {
private final UsersRepository usersRepository;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
public void register(RegisterDTO registerDTO) {
String email = registerDTO.getEmail();
String password = registerDTO.getPassword();
// 받아온 비밀번호 정보를 암호화
registerDTO.setPassword(bCryptPasswordEncoder.encode(registerDTO.getPassword()));
Users users = new Users();
Users user = users.createUser(registerDTO);
usersRepository.save(user);
}
public boolean checkEmailDuplication(String email) {
return usersRepository.existsByEmail(email);
}
public boolean checkNicknameDuplication(String nickname) {
return usersRepository.existsByNickname(nickname);
}
}
3. emailVerificationService
- createCode 메소드 -
1) 난수에 사용될 문자들을 미리 선언 후 RANDOM 클래스의 nextInt 메소드를 사용하여 문자 길이만큼의 수 내에서 랜덤 숫자를 생성
2) 랜덤 숫자를 다시 CHARACTERS 문자열의 인덱스로 접근하여 랜덤한 문자를 뽑아 StringBuilder 에 추가한 뒤 반복문이 다 돌면 빌드
- renderJspToString 메소드 -
1) templates 디렉토리에 있는 jsp 파일을 불러와 Resource 객체로 생성
2) 생성된 객체의 정보를 UTF-8 으로 인코딩된 String 으로 변환
3) String 으로 변환된 내용 중 ${verificationCode} 문자를 실제 인증 코드로 변경
- sendEmail 메소드 -
1) 이메일을 전송하기 위해 Mimemessage 객체를 생성
2) Mimemessage 를 쉽게 설정하도록 도와주는 헬퍼 클래스인 MimemessageHelper 클래스를 선언하여 다음과 같이 설정
true : HTML 형식을 지원하도록 설정
UTF-8 : 이메일 내용의 인코딩 방식을 설정
3) 헬퍼 플래스를 사용하여 이메일 주소, 제목, 내용을 다음과 같이 설정
setTo : 수신자 이메일 작성
setSubject : 메일 제목 설정
setText : 메일 본문을 설정, true 로 HTML 형식 적용
setFrom : 발신자의 이메일 주소를 설정
package com.randomchat.main.service.email;
import com.randomchat.main.domain.email.EmailVerification;
import com.randomchat.main.repository.EmailVerificationRepository;
import com.randomchat.main.repository.UsersRepository;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.SecureRandom;
import java.util.Random;
@Service
@RequiredArgsConstructor
public class EmailVerificationService {
private final UsersRepository usersRepository;
private final EmailVerificationRepository emailVerificationRepository;
private final ResourceLoader resourceLoader;
private final JavaMailSender mailSender;
private static final Random RANDOM = new SecureRandom();
private static final String CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; // 난수에 사용될 문자
public String createCode() {
StringBuilder stringBuilder = new StringBuilder();
// 10 자리 난수를 생성할 것이므로 10회 반복
for (int i = 0; i < 10; i++) {
// 난수에 사용될 문자의 길이만큼의 랜덤한 값을 추출
int randomIndex = RANDOM.nextInt(CHARACTERS.length());
// 랜덤한 index 의 문자를 추출하여 문자열로 추가
stringBuilder.append(CHARACTERS.charAt(randomIndex));
}
return stringBuilder.toString();
}
public void saveEmailVerification(String email, String verificationCode) {
EmailVerification emailVerification = new EmailVerification();
EmailVerification createEmailVerification = emailVerification.createEmailVerification(email, verificationCode);
emailVerificationRepository.save(createEmailVerification);
}
public String renderJspToString(String verificationCode) throws IOException {
// JSP 파일 내용을 String 으로 읽어오기
Resource resource = resourceLoader.getResource("classpath:/templates/emailForm.jsp");
String content = new String(Files.readAllBytes(resource.getFile().toPath()), StandardCharsets.UTF_8);
// 인증 코드를 내용에 삽입하여 리턴
return content.replace("${verificationCode}", verificationCode);
}
public void sendEmail(String email, String verificationCode) throws IOException, MessagingException {
String emailContent = renderJspToString(verificationCode);
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setTo(email);
helper.setSubject("랜덤 채팅의 이메일 인증 번호입니다.");
helper.setText(emailContent, true); // HTML 형식으로 보낼 때 true 설정
helper.setFrom("youngho3358@gmail.com");
mailSender.send(message);
}
}
4. emailForm.jsp
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Email Verification</title>
<style>
* {
text-align: center;
}
body {
font-family: Arial, sans-serif;
text-align: center;
}
h1 {
color: #4CAF50;
}
.code {
font-size: 24px;
font-weight: bold;
color: #333;
}
p {
font-size: 16px;
}
.footer {
font-size: 14px;
color: #999;
}
</style>
</head>
<body>
<h1>이메일 인증</h1>
<p>RandomChat 가입을 환영합니다!</p>
<p>아래의 인증코드를 입력하여 회원가입을 완료해주세요.</p>
<div class="code">${verificationCode}</div>
<p class="footer">감사합니다.</p>
</body>
</html>
5. application.yml 설정
![](https://blog.kakaocdn.net/dn/5IcvJ/btsKMdHGJA9/kdT0pu0kas5dpZ3eu6Wkp0/img.png)
구현 후기)
우선은 jsp 를 활용하여 치환하면 더 편할거란 생각으로 jsp 파일을 사용하였는데 단순하게 파일을 문자열로 읽어와서 문자를 치환하는 형태이기 때문에
jsp 파일을 사용할 필요가 없었다.
결국은 간단하게 txt 파일이나 html 파일로도 구현한 부분이었다...
그리고 아래는 redis 를 사용하여 구현하였을때의 장, 단점과
주 DB 인 MySQL 로 구현하였을때의 장, 단점이다.
구분 | Redis 사용 | MySQL 사용 |
---|---|---|
장점 | - 속도: 메모리 기반으로 매우 빠르게 데이터 처리 가능. - TTL 지원: 인증 코드의 유효 기간 설정을 TTL(Time To Live)로 간편하게 관리. - 부하 분산: 주 DB에 부하를 덜어주어 시스템의 성능을 최적화. |
- 통합성: 기존 시스템(DB)에 통합되어 추가적인 외부 스토리지 필요 없음. - 데이터 영속성: 인증 기록이 데이터베이스에 저장되어 추적 및 분석 가능. - 트랜잭션 지원: 인증 관련 데이터와 다른 테이블의 데이터 간 연관성을 보장. |
단점 | - 데이터 영속성 부족: Redis는 메모리 기반이므로 서버 재시작 시 데이터 손실 위험. - 추가 인프라 필요: Redis 설치 및 관리 필요. - 데이터 용량 제한: 메모리 용량에 제한이 있으므로 대규모 데이터 저장 시 비효율적. |
- 속도: Redis에 비해 상대적으로 느린 데이터 처리 속도. - 복잡성 증가: TTL 구현을 별도로 코딩해야 하며 인증 코드 만료 관리가 번거로움. - 부하 증가: 인증 관련 데이터가 많아지면 주 DB에 부하가 가중될 가능성. |
적합한 상황 | - 대량의 인증 요청을 처리해야 하는 고성능 시스템. - 인증 요청 처리 시간이 매우 중요한 서비스. |
- 인증 데이터가 비교적 적고, 인증 기록을 영속적으로 저장해야 하는 경우. - 이미 MySQL 환경에서 동작 중이며 추가적인 데이터베이스 사용이 부담스러운 경우. |
[ 배포 시에 해당 부분 코드에 문제가 발생하여 따로 글을 포스트하여 에러 픽스 부분을 기술했습니다 ]
[TIL] 2024.12.14 - 인증 코드 발송 오류 해결 ( 이메일 인증 구현 )
절벽의 바로 앞까지 가서 구경할 수 있었던 홀 슈 밴드, 마치 말 발굽 같이 생겼다고 해서 이름이 홀 슈 밴드라고 한다 - TIL -문제 발생) [TIL] 2024.11.18 - jsp 를 활용하여 이메일 인증 코드 발
youngho3358.tistory.com
'TIL' 카테고리의 다른 글
[TIL] 2024.11.21 - GCP 를 활용하여 Spring Boot 프로젝트를 배포해보기3 - 문제 발생 (3) | 2024.11.21 |
---|---|
[TIL] 2024.11.19 - GCP 를 활용하여 Spring Boot 프로젝트를 배포해보기2 (0) | 2024.11.19 |
[TIL] 2024.11.17 - GCP 를 활용하여 Spring Boot 프로젝트를 배포해보기1 (1) | 2024.11.17 |
[TIL] 2024.10.17 - Security 의 검증 값을 기준으로 User 의 정보 가져오기 (0) | 2024.10.17 |
[TIL] 2024.10.07 - [Spring Security] JWT 내부 Role 값을 확인하여 권한 처리하기 (1) | 2024.10.07 |