한국에는 판매하지 않는 기아의 텔루라이드 모델과 맥도날드의 Filet-O-Fish(필레오피쉬)

 

 

 

 

- 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 설정

gmail 의 smtp 서버를 활용

 

 

 

 

 

구현 후기)

 

우선은 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

 

 

 

 

728x90

 

728x90

+ Recent posts