본문 바로가기
개발일지

[개발일지] 이메일 전송이 왜 이렇게 느려? (feat. @Async)

by 지지 2023. 2. 15.

이메일 전송이 왜 이렇게 느려?

취준시절 RunningGo라는 작은 사이드 프로젝트를 혼자 진행했었다.
해당 프로젝트에서 회원가입, 비밀번호 찾기 시에 이메일을 전송하는 기능을 구현했는데, (당시 아주 뿌듯했다^^!)
문제는 메일을 한 번 보낼 때 마다 3~5초 정도가 소요된다는 점이었다.
회원가입을 하면 메일 전송이 완료될 때까지 몇 초간 기다려야 웰컴 페이지가 뜨는 아주 못된(?) 서비스였다.
너무 느린 메일 전송 속도에 크게 놀라 속도를 개선해 보려고 했지만 방법을 찾지 못했고.. 일단 넘어가고 나중에 수정해 보자 하고 넘긴 것이.. 취업을 하는 바람에(?) 기억 속 저 멀리에 묻히게 되었다..

그러다 실무 작업 중 또 다시 메일 전송을 구현해야 하는 상황이 왔다...! 두둥탁
메일 전송 자체는 한 번 해봤으니 크게 어려울 것이 없었다. 하지만 실제 운영에 배포될 서비스에서 메일이 전송되는 동안 사용자를 기다리게 한다는 것은 말도 안 되는 일이었다.

 

'이번엔 꼭 해결하고 마리라!'라는 마음가짐으로 '전송 속도' 자체를 개선할 수 있는 방안을 찾아다녔지만 전송 속도를 개선할 수 있는 방법을 찾지 못했다.
어떤 방법이 있을까.. 팔짱을 끼고 모니터와 눈싸움을 하며 고민을 하는데, 문득 '아니 메일 전송되는 동안 페이지만 전송완료 페이지로 넘길 수 없나??'라는 의문이 듦과 동시에 '아니 당연히 되지 멀티 쓰레드로 넘기면 되는 거 아닌가??'라는 해결책(?)도 같이 떠올랐다.

여기서 또 문제는 내가 쓰레드에 대한 지식이 너무 얕다는 점이었다. 처음 java를 배울 때만 이론 살짝 하고 간단한 예제코드만 작성해 봤지, 실제 서비스에서는 사용해 본 적이 없었다.
그렇기에 또다시 구글링을 하고, 예전에 공부했던 코드도 살펴보면서 쓰레드를 파고 있는데, 우연히 '비동기와 멀티 쓰레드의 차이점'이라는 글을 읽게 된다. 엥? 비동기는.. ajax로.. 프론트단에서 구현하는 거 아니었어...? 응.. 아니었다..


해결책!아하!

우연히 본 글을 시작으로 동기, 비동기, 싱글쓰레드, 멀티쓰레드에 대한 정보를 계속 찾아봤고, 결국엔 내가 원하는 답을 찾아냈다!

결론부터 말하면 나는 비동기-멀티쓰레드 방식을 채택하여 이메일을 보냄과 동시에(정확하게는 이메일을 보내는 동안)

다음 페이지로 넘기는 것에 성공할 수 있었다.


간단하게 설명하자면 멀티쓰레드는 여러 개의 프로세스가(일꾼이 여러명) 동시에 작업을 수행 하는 것이고,

비동기는 여러개의 작업을 순서에 상관없이 처리하는 것을 말한다.

즉, 비동기-멀티쓰레드는 '여러 명이 작업을 하는데 누가 먼저 어떤 작업을 하든 상관이 없다'는 뜻이 된다.

나의 경우에 대입해 보면 회원가입을 진행하는 쓰레드 하나, 이메일을 보내는 쓰레드 하나, 총 두 개의 쓰레드를 두고

이메일을 보내는 쓰레드가 작업함과 동시에 회원가입은 계속 진행하는(화면을 넘김) 전략을 취한 셈이다.


리팩토링을 해보자

멀티쓰레딩 방식의 비동기 처리를 하는 방법에는 순수 Java로 코딩하는 방법과 Spring이 제공해 주는 기능을 사용하는 것이 있는 것 같다.(더 있는지는 모르겠다..)

나는 스프링이 제공해주는 @Async 어노테이션을 사용해서 해당 기능을 구현했다.

 

1. 쓰레드 풀을 사용하기 위해 config 파일을 만들어준다.

@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean
    public Executor customAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5); //기본 스레드 수
        executor.setMaxPoolSize(10); //최대 스레드 수
        executor.setThreadNamePrefix("email-thread"); //쓰레드 이름..?
        executor.initialize(); // 꼭 써줘야 한다. 왜..?
        return executor;
    }
}

 

2. 멀티쓰레딩 방식의 비동기 처리가 필요한 메서드에 @Async 어노테이션을 붙인다.

기존 코드와 수정된 코드를 비교해 보자.

[기존 코드]

MemberServiceImpl.java

public int insertMember(MemberDto memberDto) throws Exception {

        //랜덤 문자열을 생성해서 mail_key 컬럼에 넣어주기
        String mail_key = new TempKey().getKey(30,false);
        memberDto.setMail_key(mail_key);
        //비밀번호를 암호화해서 넣어주기
        String encPassword = passwordEncoder.encode(memberDto.getPass());
        memberDto.setPass(encPassword);
        //회원가입
        int result = memberDao.insertMember(memberDto);
        memberDao.updateMailKey(memberDto);

        //회원가입 완료하면 인증을 위한 이메일 발송
        MailHandler sendMail = new MailHandler(javaMailSender);
        sendMail.setSubject("[RunninGo 이메일 인증메일 입니다.]"); //메일제목
        sendMail.setText(
                "<h1>RunninGo 메일인증</h1>" +
                "<br>RunninGo에 오신것을 환영합니다!" +
                "<br>아래 [이메일 인증 확인]을 눌러주세요." +
                "<br><a href='http://localhost:8080/join/registerEmail?email=" + memberDto.getEmail() +
                "&mail_key=" + mail_key +
                "' target='_blank'>이메일 인증 확인</a>");
        sendMail.setFrom(myInfo.runningGoId, "러닝고");
        sendMail.setTo(memberDto.getEmail());
        sendMail.send();

        log.info("회원가입 인증 메일 발송 성공");

        return result;
    }

insertMember() 메서드 안에 회원가입 로직과 이메일 전송 로직이 함께 들어있다.

이 코드로는 회원가입 후 메일 전송까지 완료되어야만 컨트롤러에서 view로 넘어갈 수 있다.

 

[수정된 코드]

public int insertMember(MemberDto memberDto) throws Exception {
		//랜덤 문자열을 생성해서 mail_key 컬럼에 넣어주기
        String mail_key = new TempKey().getKey(30,false);
        memberDto.setMail_key(mail_key);
        //비밀번호를 암호화해서 넣어주기
        String encPassword = passwordEncoder.encode(memberDto.getPass());
        memberDto.setPass(encPassword);
        //회원가입
        int result = memberDao.insertMember(memberDto);
        memberDao.updateMailKey(memberDto);
        return result;
    }
    
    @Async
    @Override
    public void sendJoinCertificationMail(MemberDto memberDto) throws MessagingException, UnsupportedEncodingException {
            //회원가입 완료하면 인증을 위한 이메일 발송
            MailHandler sendMail = new MailHandler(javaMailSender);
            sendMail.setSubject("[RunninGo 이메일 인증메일 입니다.]"); //메일제목
            sendMail.setText(
                    "<h1>RunninGo 메일인증</h1>" +
                    "<br>RunninGo에 오신것을 환영합니다!" +
                    "<br>아래 [이메일 인증 확인]을 눌러주세요." +
                    "<br><a href='http://localhost:8080/join/registerEmail?email=" + memberDto.getEmail() +
                    "&mail_key=" + memberDto.getMail_key() +
                    "' target='_blank'>이메일 인증 확인</a>");
            sendMail.setFrom(myInfo.runningGoId, "러닝고");
            sendMail.setTo(memberDto.getEmail());
            sendMail.send();

            log.info("회원가입 인증 메일 발송 성공");
    }

 

수정된 코드는 회원가입만을 위한 메서드(insertMember), 이메일만을 전송하는 메서드(sendJoinCertificationMail)로 분리하였다. 그러면서 이메일 전송 메서드에는 @Async 어노테이션이 붙어 있는 것을 확인할 수 있다!

 

3. 위 두 메서드 호출하기

JoinController.java

@PostMapping("/joinCheck")
    public String joinCheck(@Valid MemberDto memberDto, Errors errors, Model model) throws Exception{

        //만약 회원가입에 실패한다면
        if (errors.hasErrors()) {
            return "/member/joinForm";
        }

        //유효성 검사를 통과하면 insert 후 페이지 이동
        int result = memberService.insertMember(memberDto);
        if(result == 1){
            memberService.sendJoinCertificationMail(memberDto); //인증메일 보내기
            return "/member/joinSuccessForm";
        }
        return "/member/joinForm";
    }

기존 코드였다면 sendJoinCertificationMail() 메서드가 따로 없고 바로 위 insertMember() 메서드에서 모든 로직이 실행되었을 것이다. 2번 과정에서 메서드를 분리한 덕분에 insertMember() 메서드가 호출되면서 바로 sendJoinCertificationMail()가 호출되는데!!!

여기가 중요하다. 멀티쓰레드-비동기 처리를 했기 때문에 sendJoinCertificationMail() 메서드는 다른 쓰레드를 사용해 실행하게 되면서, 기존 실행되고 있던 쓰레드는 더 이상 메일이 보내지는 것을 기다리지 않고 return문으로 넘어가 바로 웰컴페이지(joinSuccessForm)로 넘어갈 수 있게 되었다!

 

요약하자면,

기존 : 하나의 메서드에 모든 로직을 넣으면서 하나의 쓰레드만 일을 시켜 메일 전송이 완료되는 시점에 페이지가 넘어감.

현재 : 멀티쓰레드-비동기 처리를 함으로써 두 개의 쓰레드가 일하며 메일이 보내지는 동안 페이지가 넘어감.

이렇게 개선이 되었다.

(그런데.. 사실 위 코드에는 메일 전송 실패에 대한 예외처리 코드가 없다. 추후 예외 처리도 추가해보도록 하겠다.)


마무리

이렇게 메일 전송 속도 자체의 개선은 아니었지만, 스스로 생각해 낸 방법으로 문제를 해결해 보았다.

근데 사실 이게 정답인지 아닌지는 모르겠다.. 물어보고 싶은데.. 물어볼 사람이 없다..(피드백은 언제나 환영합니다..)

그런데, 사실 생각해 보면 평소에 어느 사이트에서 비밀번호를 찾기 위해 이메일 인증을 선택하면, 메일함에 가봐도 메일이 와있지 않아 새로고침을 몇 번 눌렀던 기억이 한두 번이 아니다!! 또한 전체 공지 메일을 받을 때도 누구는 지금 받았네, 누구는 몇 분 뒤에 받았네 했던 기억도 난다. 이메일 전송이라는 게 애초에 속도 자체가 느린 거라고 생각되기도 한다!

 

꼭 풀어보고 싶은 숙제였는데, 드디어 풀게 되어 시원하다. 단지 올바른 방식인지 판단을 못하는 게 찝찝하다..
더 배워가면서 이 찝찝함도 날려버려야지.. 그리고 나는 예외처리가 너무 어려운데.. 공부를 더 해야겠다는 생각이 든다.

(이 카테고리 잘 만든 것 같아 재밌어 짜릿해)



[참고 블로그]

https://jcchu.medium.com/동기-비동기-쓰레드-멀티쓰레드
https://steady-coding.tistory.com/611

반응형

댓글