개발놀이터

온라인 쇼핑몰 ver.2 (2) : SMTP 비동기 통신으로 바꾸기 본문

사이드 프로젝트/온라인 쇼핑몰 ver.2

온라인 쇼핑몰 ver.2 (2) : SMTP 비동기 통신으로 바꾸기

마늘냄새폴폴 2023. 5. 16. 13:41

기존 SMTP

  • SMTP 는 stateful 프로토콜 + 동기 네트워킹입니다. 

 

기존 방식의 문제점

  • 이메일은 회원가입시 2FA로 이용됩니다. 회원 가입을 할 때 이메일을 보내면 이메일이 전송되는데 걸리는 시간인 3~4초 가량을 아무것도 할 수 없습니다. 
  • 이는 부정적인 유저 경험으로 이어질 우려가 있습니다. 

 

ver.2에서 개선한 SMTP

  • SMTP를 @Async 어노테이션으로 비동기 처리를 하였습니다. 
  • 기존 방식대비 고객경험을 90배가량 개선할 수 있었습니다. 

 

 

먼저 JavaMailSender를 빈으로 등록해줬습니다.

package com.hello.capston.config;

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;

@Configuration
public class JavaMailSenderConfig {

    private String protocol = "smtp";
    private boolean auth = true;
    private boolean starttls = true;
    private boolean debug = true;
    private String host = "smtp.gmail.com";
    private int port = 587;
    private String username = "gmail 계정";
    private String password = "패스워드";
    private String encoding = "UTF-8";

    @Bean
    public JavaMailSender javaMailSender() {
        JavaMailSenderImpl javaMailSender = new JavaMailSenderImpl();
        Properties properties = new Properties();

        // properties
        properties.put("mail.transport.protocol", protocol);
        properties.put("mail.smtp.auth", auth);
        properties.put("mail.smtp.starttls.enable", starttls);
        properties.put("mail.smtp.debug", debug);

        // mail sender
        javaMailSender.setHost(host);
        javaMailSender.setPort(port);
        javaMailSender.setUsername(username);
        javaMailSender.setPassword(password);
        javaMailSender.setJavaMailProperties(properties);
        javaMailSender.setDefaultEncoding(encoding);
        javaMailSender.setProtocol(protocol);
        return javaMailSender;
    }
}

 

 

@Async를 위한 서브 스레드를 만들어줬습니다. 서브 스레드는 AsyncConfigurer를 구현한 Configuration 클래스입니다. 

package com.hello.capston.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.Executor;

@Configuration
@EnableAsync
@Slf4j
public class AsyncConfiguration implements AsyncConfigurer {

    @Override
    @Bean(name = "mailExecutor")
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(5);
        executor.setQueueCapacity(10);
        executor.setThreadNamePrefix("MailExecutor-");
        executor.initialize();
        return executor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return (ex, method, params) -> {
            log.error("Exception handler for async method : ", method.toGenericString(), " threw unexpected exception itself", ex);
        };
    }
}

 

 

이메일을 보낼 Service 클래스를 만들어줬습니다. 

package com.hello.capston.service.email;

import lombok.RequiredArgsConstructor;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.mail.javamail.MimeMessagePreparator;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class MailService {

    private final JavaMailSender mailSender;

    @Async("mailExecutor")
    public void sendMail(String to, String subject, String checkNum) {
        System.out.println("[sendMail :: " + Thread.currentThread().getName() + "]");

        MimeMessagePreparator messagePreparator = mimeMessage -> {
            final MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "UTF-8");
            helper.setFrom("kyoungsuk3254@gmail.com");
            helper.setTo(to);
            helper.setSubject(subject);
            helper.setText(checkNum);
        };

        mailSender.send(messagePreparator);
    }
}

 

기존 SMTP가 동기 네트워킹으로 돌아가는 것을 확인했고 시간은 4.245초가 걸렸습니다. 

 

 

비동기 네트워킹을 처리하고 나서 제가 작성한 서브 스레드로 돌아가는 것을 확인했고 시간은 0.06초가 걸렸습니다. 

 

 

느낀점

 

스프링에서 동기 네트워킹으로 작동하던 SMTP를 비동기 통신으로 바꾸면서 단순히 성능이 빨라지는 것만이 고객경험을 긍정적으로 바꾸는 것이 아니라는 것을 깨달았습니다. 

 

동기 네트워킹으로 작동하던 것을 비동기 네트워킹으로 바꾸는 것은 개발자 입장에선 조금 불편하지만 고객의 입장에선 정말 속도가 빠른 것을 체감하는 긍정적인 경험으로 이어진다는 것을 보며 고객의 경험을 우선시해야하는 개발자로서 더 노력해야 한다는 것을 알게되었습니다.