좌충우돌 개발공부

TIL(20240607) [뉴스피드 프로젝트 : 이메일 가입 및 인증 기능]


📌 Spring

💡 뉴스피드 프로젝트 : 이메일 가입 및 인증 기능

  • 이메일 가입 시, 이메일 인증 기능을 추가 Step 1 : 사용자가 가입한 이메일 주소로 인증번호 발송
    Step 2 : 발송한 인증번호와 입력란의 인증번호가 일치하는 지 확인
    Step 3 : 이메일 인증이 완료되지 않은 회원들의 회원상태코드를 ‘인증 전’ 으로 설정

이메일 가입 및 인증기능을 구현하기 위해 이전에 프로필 부분을 수정해야 햇는데, 프로필에서 이메일을 수정할 수 있는 기능이 있어서 이메일은 수정할 수 없도록 하고 유저 엔티티에서 이메일 부분을 @Notnull로 변경이 필요했다.

그래서 컬럼에는 nullable=false라 되어 있는데, DB에서는 DDL생성 시 not null로 생성 되겠지만 entity에서도 검증이 연결될까? 라는 의문이 생겼다. 그래서 찾아본 것이 컬럼에 nullable=false하지 않고 Validation- @NotNull로 해주면 자동으로 DDL생성 시 NotNull이 된다는 것을 알게되었다.

Jpa:nullable=false와 @NotNull

본격적으로 이메일 인증 구현을 해보려고 찾아보니, gradle에 의존성 추가가 필요하다.


implementation 'org.springframework.boot:spring-boot-starter-mail'

그 다음, 구글 Gmail의 SMTP설정(IMAP)을 하고 application.properties의 설정을 해주면 끝! IMAP 설정방법 및 앱 비밀번호 생성은 구글에 검색하면 해당 내용들을 확인할 수 있다.

이메일 전송-SMTP Google Gmail SMTP 설정방법 및 메일 전송1 Google Gmail SMTP 설정방법 및 메일 전송2


spring.mail.properties.mail.smtp.timeout=3000 // 3분 만료
spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.username={구글이메일주소}
spring.mail.password={ 비밀번호}
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.ssl.protocols=TLSv1.2
spring.mail.properties.mail.smtp.ssl.trust=smtp.gmail.com

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/user")
public class UserController {

    private final UserService userService;
    private final JwtUtil jwtUtil;

    // 인증번호 발송
    @PostMapping("/email")
    public ResponseEntity<String> sendMail(@RequestBody @Valid EmailRequestDto requestDto) throws Exception {
        userService.sendMail(requestDto);
        return ResponseEntity.ok("인증번호 발송완료");
    }

    // 인증번호 일치여부 확인
    @GetMapping("/email/verify")
    public ResponseEntity<String> checkCode(@RequestBody @Valid VerifyCodeRequestDto requestDto) {
        userService.checkCode(requestDto);
        return ResponseEntity.ok("인증이 완료되었습니다.");
    }

아 사실 기획설계가 명확하게 되지 않은 상태라 이 부분에서 정말 고민을 많이 했다. 보통의 회원가입을 보면 인증번호 발송 버튼을 누르면 인증번호가 발송이 되고 인증번호 일치여부 확인 또한 인증번호 확인 버튼이 있는 경우를 많이 봐서 API를 3개로 설계해야겠다고 생각했다.[1) 인증번호 발송 2) 인증번호 확인 3) 회원가입]

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final JavaMailSender javaMailSender;
    private static final String senderEmail = "ReviewSpot@gmail.com";
    private static int code;
    private final Map<String, String> codes = new HashMap<>();

    // 인증번호 생성
    public static String createCode() {
        int code = (int) (Math.random() * (90000)) + 100000;
        return String.valueOf(code);
    }

    // 이메일 생성
    public MimeMessage createMail(String email, String code) throws MessagingException {
        createCode();
        MimeMessage message = javaMailSender.createMimeMessage();
        MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");

        try {
            helper.setFrom(senderEmail);
            helper.setTo(email);
            helper.setSubject("ReviewSpot [회원가입을 위한 이메일 인증]");
            String body = "";
            body += "<h3>" + "이메일 인증 번호" + "</h3>";
            body += "<h1>" + code + "</h1>";
            body += "<h3>" + "위 인증번호를 입력해주세요." + "</h3>";
            helper.setText(body, true);
        } catch (MessagingException e) {
            e.printStackTrace();
        }
        return message;
    }

    // 이메일 전송
    public void sendMail(EmailRequestDto requestDto) throws MessagingException {
        String code = createCode();
        // 인증번호 임시저장
        codes.put(requestDto.getEmail(), code);
        MimeMessage message = createMail(requestDto.getEmail(), code);

        javaMailSender.send(message);
    }

    // 인증번호 검증
    public boolean checkCode(VerifyCodeRequestDto requestDto) {
        String email = requestDto.getEmail();
        String inputCode = requestDto.getVerificationCode();
        String checkCode = codes.get(email);

        if ((!checkCode.isEmpty()) && checkCode.equals(inputCode)) {
            return true;
        } else {
            throw new IllegalArgumentException("인증번호가 일치하지 않거나 만료된 인증번호 입니다.");
        }
    }

    // 회원가입
    public void signup(SignupRequestDto requestDto) {
        String userId = requestDto.getUserId();
        String password = passwordEncoder.encode(requestDto.getPassword());
        String userName = requestDto.getUserName();
        String email = requestDto.getEmail();
        String verificationCode = requestDto.getVerificationCode();
        UserStatus userStatus = UserStatus.NOT_AUTH;

        // 회원 중복 확인
        Optional<User> checkUsername = userRepository.findByUserId(userId);
        if (checkUsername.isPresent()) {
            throw new IllegalArgumentException("중복된 사용자가 존재합니다.");
        }

        // 이메일 중복 확인
        Optional<User> checkEmail = userRepository.findByEmail(email);
        if (checkEmail.isPresent()) {
            throw new IllegalArgumentException("중복된 Email 입니다.");
        }

        // 이메일 인증 완료 시 회원가입
        VerifyCodeRequestDto verifyCodeRequestDto = new VerifyCodeRequestDto(email, verificationCode);
        if (checkCode(verifyCodeRequestDto)) {
            userStatus = UserStatus.MEMBER;
            User user = new User(userId, password, userName, email, userStatus);
            userRepository.save(user);
        }
    }

랜덤으로 난수 생성(인증코드) 후에 요청한 이메일과 인증코드를 MAP에 임시저장을 하고 메일을 보낼 때 MimeMessage를 통해서 해당 이메일에 코드를 넣어 JavaMailSender로 보내는 형식으로 구현해보았다.

인증번호는 Map에 임시저장했던 email key를 통해 code를 찾아 inputcode와 일치하면 인증처리 하도록 구현했다. Map을 통해 구현한 이유를 묻는다면, 변명을 보태 사실 시간이 부족해서였다. 팀원들과 같이 프로젝트를 마감기한을 정해 제출하기 전날 코드리뷰와 코드리팩토링을 하기로 약속했기에 무조건 이번주 일요일까지 구현을 해야 했던 터라 DB저장 형식으로 해보고 싶었지만 다음 번에 도전해보기로 했다.

그리고 어려웠던 부분을 얘기하자면 SMTP 같은 경우는 버전이 다양해 버전이 하나라도 맞지 않으면 실행이 되지 않는다는 글을 많이 보았던 것 같다. 나 역시도 아래와 같은 에러에 직면했고 3시간 동안 원인을 찾기 위해 고군분투했다.


"Error": "Mail server connection failed. Failed messages: jakarta.mail.NoSuchProviderException: smtp"

application.properties에 아래 내용을 추가했더니.. 성공… 😂 울뻔했다… 하하핳 사실 결국에는 튜터님의 도움을 받았다. 5시간 동안 버전을 찾아 적용하고 테스트를 반복했는데 도무지 어떤 부분에서 잘못되었는지 파악하지 못했기 때문이다. 다음번에는 이런 불상사가 생기지 않도록 수동으로 SMTP 설정하도록 해야겠다.


spring.mail.properties.mail.smtp.ssl.trust=smtp.gmail.com


public enum UserStatus {
    MEMBER("정상 회원"),
    NON_MEMBER("탈퇴 회원"),
    NOT_AUTH("인증 전");

@Getter
public class VerifyCodeRequestDto {
    @Email(message = "유효한 이메일을 입력해주세요.")
    @NotBlank(message = "Email을 입력해주세요.")
    private String email;

    @NotNull(message = "Email로 발송된 인증번호를 입력해주세요.")
    private String verificationCode;

    public VerifyCodeRequestDto(String email, String verificationCode) {
        this.email = email;
        this.verificationCode = verificationCode;
    }
}

@Getter
public class EmailRequestDto {
    @Email(message = "유효한 이메일을 입력해주세요.")
    @NotBlank(message = "Email을 입력해주세요.")
    private String email;
}

이메일 인증 구현은 정말 다양한 방법이 많은 것 같았다. jwt로 토큰을 생성하여 사용자정보를 토큰에 넣어 일치하는지 확인하는 형식도 찾아보니 있었다. 그리고 인증 만료시간을 구현하기 위해 최적화 된 Redis로 구현된 것도 보았는 것 같다. 사실 깊이 공부를 하지 못해서 저런 것들이 있구나.. 하고 넘어간 것 같다.

댓글 좋아요 기능 구현도 남았기에 마감기한에 제출할 수 없을 것 같아서 다음에 나의 개인 프로젝트에 토큰으로 이메일 인증구현을 해보려고 한다. 빠샤빠샤🦾


📌 코딩테스트1️⃣ : 행렬의 덧셈

🔒 문제 : 행렬의 덧셈은 행과 열의 크기가 같은 두 행렬의 같은 행, 같은 열의 값을 서로 더한 결과가 됩니다. 2개의 행렬 arr1과 arr2를 입력받아, 행렬 덧셈의 결과를 반환하는 함수, solution을 완성해주세요.

🚫 조건 :

  • 행렬 arr1, arr2의 행과 열의 길이는 500을 넘지 않습니다.

🔓 문제풀이

class Solution {
    public int[][] solution(int[][] arr1, int[][] arr2) {
        int[][] answer = {};
        answer = new int[arr1.length][arr1[0].length]; // [세로][가로] 그리고 arr1[][] == arr2[][]
        
        for (int i=0; i<arr1.length; i++){ // 세로
            for (int j=0; j<arr1[i].length; j++){ // 가로
                answer[i][j] = arr1[i][j] + arr2[i][j];
            }
        }
        return answer;
    }
}