[Spring] Naver (STMP) 메일 보내기 및 테스트 작성 - SJ Coding Helper 프로젝트 리펙토링 (3)

TaesunPark·2023년 1월 18일
0

자세한 코드는 깃허브 참조 바랍니다.
https://github.com/TaesunPark/SejongCodingHelper

기존 회원가입 플로우

1. 중복 이메일 있는 지 확인
2. Gmail 메일을 이용해서 random code 보내기 (random code에 대한 session 값 저장 (10분 설정))
3. 검증 코드 확인
4. DB 저장

구글 stmp 서비스 유료화

→ 네이버 stmp 서비스로 변경

변경 회원가입 플로우
1. 중복 이메일이 있는 지 확인
2. Gmail -> Naver (유료화 이슈로 변경) random code 보내기
3. 검증 코드 확인
4. id 중복 값 확인
5. DB 저장

기존 SignupContoller 클래스 코드

@Slf4j
@RestController
@RequiredArgsConstructor
//@CrossOrigin(origins = "http://localhost:3000", allowCredentials = "true")
@CrossOrigin(origins = Constants.URL , allowCredentials = "true")
public class SignupController {

    private final JavaMailSender javaMailSender;
    private final UserService userService;
    private final ChatbotRoomService chatbotRoomService;

    @PostMapping("/sendSejongEmail")
    public void sendSejongEmail(@RequestBody Map<String, String> map, HttpServletRequest request) throws MessagingException {

        String email = map.get("email") + "@sju.ac.kr";

        // 키값 생성
        String authCode = getAuthCode();

        String content = "<h4>안녕하세요.</h4><h4>Sejong Coding Helper입니다.</h4>" + "<h4>세종대학교 이메일 인증을 위해서 아래 인증 코드를 입력해주세요.</h4>" +
                "<h2>인증 코드 : " + "<b><u>" + authCode + "</u></b><h2>" + "<h4>감사합니다.</h4>";

        // 메일 보내기
        MimeMessage message = javaMailSender.createMimeMessage();

        message.setFrom("Sejong Coding Helper<sjhelper10@gmail.com>");
        message.setSubject("Sejong Coding Helper 회원가입 인증 메일");
        message.setRecipient(Message.RecipientType.TO, new InternetAddress(email));
        message.setText(content, "UTF-8", "html");
        message.setSentDate(new Date());

        javaMailSender.send(message);

        HttpSession session = request.getSession();
        session.setMaxInactiveInterval(60 * 10);  //10분
        session.setAttribute("authCode", authCode);

        System.out.println(authCode);
    }

    @PostMapping("/checkEmailAuthCode")
    public String checkEmailAuthCode(@RequestBody Map<String, String> map, HttpServletRequest request) {
        HttpSession session = request.getSession();
        String authCode = (String) session.getAttribute("authCode");
        String inputedAuthCode = map.get("authCode");
        System.out.println(authCode + "," + inputedAuthCode);

        if (authCode.equals(inputedAuthCode))
            return "accepted";
        else
            return "fail";

    }

    @PostMapping("/completeUserSignup")
    public String completeUserSignup(@RequestBody Map<String, String> map) {

        //학번 중복 확인
        if (userService.isOverlapStudentNumber(map.get("studentNumber"))) {
            return "denied";
        }
        // db에 저장하는 구문
        long id = userService.signUp(new UserDTO2(map.get("studentNumber"), map.get("pwd"), map.get("name"), map.get("email")));
        System.out.println(id);
        chatbotRoomService.create(new ChatbotRoomDTO(id,4L,"C", "0"));
        chatbotRoomService.create(new ChatbotRoomDTO(id,4L,"P", "0"));

        return "accepted";
    }

    @PostMapping("/checkEmailOverlap")
    public String checkEmailOverlap(@RequestBody Map<String, String> map) {
        if (userService.isOverlapEmail(map.get("email") + "@sju.ac.kr"))
            return "denied";
        return "accepted";
    }

    @PostMapping("/checkIdOverlap")
    public void checkIdOverlap(@RequestBody Map<String, String> map) {

    }

    //인증코드 난수 발생
    private String getAuthCode() {
        Random random = new Random();
        StringBuffer buffer = new StringBuffer();
        int num = 0;

        while (buffer.length() < 6) {
            num = random.nextInt(10);
            buffer.append(num);
        }

        return buffer.toString();
    }

}

기존 SignupContoller 문제점 및 해결 방안

1. Contoller에 맞지 않는 책임들이 너무 많다. 그리고 컨트롤러에 맞지 않는 책임들이 많다.
		-> 비즈니스 코드에 대한 부분들은 Service로 빼줌으로써 책임을 분리시켜준다.

2. 단순히 요청 바디에 대한 내용을 Map으로 받는다.
		-> 요청 바디에 대한 데이터들을 Dto로 빼줌으로써 어떤 데이터가 들어오는 지 명확하게 해준다.

3. 응답하는 부분이 void, String이다. 너무 불친절하다.
		-> 응답 메시지에 대한 부분을 규격화하여 명세를 만들어주고, 바꿔준다.

4. restful하지 않다. 
		-> 요청 url이 /sendSejongEmail 동사 형태다. 명사로 바꿔준다.

5. 에러를 핸들링하지 않는다.
		-> 에러가 발생하면 불친절하게 대응이 된다.

sendSejongEmail 메소드 리펙토링

기존 코드

    @PostMapping("/sendSejongEmail")
    public void sendSejongEmail(@RequestBody Map<String, String> map, HttpServletRequest request) throws MessagingException {

        String email = map.get("email") + "@sju.ac.kr";

        // 키값 생성
        String authCode = getAuthCode();

        String content = "<h4>안녕하세요.</h4><h4>Sejong Coding Helper입니다.</h4>" + "<h4>세종대학교 이메일 인증을 위해서 아래 인증 코드를 입력해주세요.</h4>" +
                "<h2>인증 코드 : " + "<b><u>" + authCode + "</u></b><h2>" + "<h4>감사합니다.</h4>";

        // 메일 보내기
        MimeMessage message = javaMailSender.createMimeMessage();

        message.setFrom("Sejong Coding Helper<sjhelper10@gmail.com>");
        message.setSubject("Sejong Coding Helper 회원가입 인증 메일");
        message.setRecipient(Message.RecipientType.TO, new InternetAddress(email));
        message.setText(content, "UTF-8", "html");
        message.setSentDate(new Date());

        javaMailSender.send(message);

        HttpSession session = request.getSession();
        session.setMaxInactiveInterval(60 * 10);  //10분
        session.setAttribute("authCode", authCode);

        System.out.println(authCode);
    }

변경 코드


@PostMapping("/email")
    public ResponseEntity<SuccessResponse<SendEmailResponse>> sendSejongEmail(@RequestBody SendEmailRequest sendEmailRequest, HttpServletRequest request) throws MessagingException {
        return SuccessResponse.success(SuccessCode.EMAIL_SUCCESS, emailService.sendSejongEmail(sendEmailRequest, request));
    }

sendSejongEmail 변경 부분

1.  이메일 전송에 대한 코드를 EmailService에 책임을 분리 시켰다.
2.  SendEmailRequest DTO를 만들어줌으로써 어떤 요청 데이터가 필요한 지 명확하게 해줬다.
3.  이메일 전송 성공에 대한 코드를 규격화 시켜줬다.

자세한 코드는 깃허브 참고하세요.

EmailService 책임 분리

1. JavaMailSender에 대한 구현체를 Bean으로 등록해서 사용
2. EmailService 내부 메소드에 sendEmail, setSession 구분해줌으로써 메소드에 대한 책임 분리.
setSession은 추후에 책임 분리 EmailService에서 분리될 예정 (지금은 어떻게 분리할 지 고민중.)

JavaMailSender 구현체 Bean 등록

package com.example.testlocal.config.mail;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.JavaMailSenderImpl;

import java.util.Properties;

@Slf4j
@Configuration
public class EmailConfig {
    @Value("${spring.mail.host}")
    String host;

    @Value("${spring.mail.username}")
    String username;

    @Value("${spring.mail.password}")
    String password;

    @Bean(name = "MyMailService")
    public JavaMailSender javaMailService() {
        JavaMailSenderImpl javaMailSender = new JavaMailSenderImpl();
        javaMailSender.setUsername(username);
        javaMailSender.setHost(host);
        javaMailSender.setPassword(password);
        javaMailSender.setPort(465);
        javaMailSender.setJavaMailProperties(getMailProperties());
        return javaMailSender;
    }

    private Properties getMailProperties() {
        Properties properties = new Properties();
        properties.setProperty("mail.protocol", "smtps");
        properties.setProperty("mail.smtp.auth", "true");
        properties.setProperty("mail.debug", "true");
        properties.setProperty("mail.smtp.ssl.trust","smtp.naver.com");
        properties.setProperty("mail.smtp.ssl.enable","true");
        return properties;
    }
}

EmailService sendEmail 구현

package com.example.testlocal.module.user.application.service;

import com.example.testlocal.core.dto.SuccessResponse;
import com.example.testlocal.module.user.application.dto.SendEmailRequest;
import com.example.testlocal.module.user.application.dto.response.SendEmailResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.util.Date;
import java.util.Map;
import java.util.Random;

@Service
public class EmailService{
    private final JavaMailSender javaMailService;

    @Value("${spring.mail.username}")
    private String from;

    public EmailService(JavaMailSender javaMailService) {
        this.javaMailService = javaMailService;
    }

    public SendEmailResponse sendSejongEmail(SendEmailRequest sendEmailRequest, HttpServletRequest request) throws MessagingException {
        String authCode = getAuthCode();
        sendEmail(sendEmailRequest.getEmail(), authCode);
        setSession(request, authCode);

        return SendEmailResponse.of(sendEmailRequest.getEmail());
    }

    public void sendEmail(String email, String authCode) throws MessagingException {
        email += "@sju.ac.kr";

        String content = "안녕하세요. Sejong Coding Helper입니다.\n" + "세종대학교 이메일 인증을 위해서 아래 인증 코드를 입력해주세요.\n" +
                "인증 코드 : " + authCode +"\n감사합니다.";

        // 메일 보내기
        MimeMessage message = javaMailService.createMimeMessage();
        MimeMessageHelper h = new MimeMessageHelper(message,"UTF-8");

        h.setFrom(from);
        h.setTo(email);
        h.setSubject("Sejong Coding Helper 회원가입 인증 메일");
        h.setText(content);
        h.setSentDate(new Date());
        javaMailService.send(h.getMimeMessage());
    }

    public void setSession(HttpServletRequest request, String authCode){
        HttpSession session = request.getSession();
        session.setMaxInactiveInterval(60 * 10);  //10분
        session.setAttribute("authCode", authCode);
    }

    public String randomCode(){
        return String.valueOf((int)(Math.random() * (999999 - 100000 + 1)) + 100000);
    }

    //인증코드 난수 발생
    private String getAuthCode() {
        Random random = new Random();
        StringBuffer buffer = new StringBuffer();
        int num = 0;

        while (buffer.length() < 6) {
            num = random.nextInt(10);
            buffer.append(num);
        }

        return buffer.toString();
    }

}

EmailService Test 코드 추가

package com.example.testlocal.module.user.application.service;

import com.example.testlocal.config.mail.EmailTestConfig;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.test.context.junit.jupiter.SpringExtension;

import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;

import java.util.Date;

import static org.assertj.core.api.Assertions.assertThat;

@ExtendWith({SpringExtension.class})
@Import({EmailTestConfig.class})
class EmailServiceTest {

    @InjectMocks
    private EmailService emailService;

    @Autowired
    private JavaMailSender MyMailService;

    @DisplayName("이메일 보내는 테스트 결과 값은 메일 확인")
    @Test
    public void sendEmail() throws MessagingException {
        String email = "tovbskvb@sju.ac.kr";

        String content = "test";

        // 메일 보내기
        MimeMessage message = MyMailService.createMimeMessage();

        MimeMessageHelper h = new MimeMessageHelper(message,"UTF-8");

        h.setFrom("sjcodinghelper@naver.com");
        h.setTo(email);
        h.setSubject("Sejong Coding Helper 회원가입 인증 메일");
        h.setText(content);
        h.setSentDate(new Date());
        MyMailService.send(h.getMimeMessage());
    }

    @DisplayName("랜덤 값 확인 테스트")
    @Test
    public void random() throws MessagingException {
        String value1 = emailService.randomCode();
        assertThat(value1).isNotSameAs(emailService.randomCode());
    }

}

에러날 부분 생각.

여기서 에러날 부분이면

javaMailService.send(h.getMimeMessage());

이 부분이다. 그래서 try catch로 감싸준다음 에러를 만들어줄까 생각을 했는데

JavaMailSender 를 까보니까

void send(MimeMessage mimeMessage) throws MailException;

에러가 나면 MailException을 던져주고

org.springframework.mail.MailAuthenticationException: Authentication failed; nested exception is javax.mail.AuthenticationFailedException: 535 5.7.1 Username and Password not accepted giOy4IPsRQu+TYD--YNvNQ - nsmtp

이렇게 상세한 정보까지 남겨주기 때문에 상태 코드만으로 외부적 서버 에러가 발생했다고 클라이언트에 던져주는 작업만 실행했다.

{
    "status": 500,
    "success": false,
    "message": "예상치 못한 서버 에러가 발생하였습니다."
}

0개의 댓글