2 minute read

이메일 전송하기

Spring에서 제공하는 JavaMailSender 인터페이스를 이용하면 메일 서비스를 정말 쉽게 구현할 수 있다.

심지어 spring-boot를 사용한다면 아래 사진과 같이 properties 설정만 해주어도 JavaMailSender Bean을 자동으로 등록해준다.

image

또한 별도의 서버를 구축하지 않고도, 구글이나 네이버에서 SMTP 서버를 제공해주므로 간편하게 구현할 수 있다. 체크메이트는 구글 계정을 생성해 Gmail의 SMTP 서버를 활용하였다.


비동기 처리를 해보자

Spring에서 MailSender도 제공해주고 Goolge에서 SMTP 서버도 제공해주고 정말 수월하게 진행을 하고 있었는데 문제가 발생했다.

  • 이메일 전송까지 완료가 되어야 클라이언트로 HTTP 응답을 보내줄 수 있기 때문에, 브라우저에는 약 4초동안 아무 반응이 없었다. 사용자가 충분히 불편해할 것이고 버튼이 안 눌러진줄 알고 또 다시 인증 요청 버튼을 누를 확률이 높다.
  • Gmail SMTP 서버가 중단되어 통신할 수 없는 상황이라면, 체크메이트 서버 입장에서는 계속 대기하게 될 것이다. 여러 이메일 인증 요청이 들어오게 된다면, 여러 스레드는 대기하게 될 것이고 WAS의 성능이 감소될 수도 있다.
  • 심지어 이메일 인증 요청 로직이 @Transactional 안에 포함되어 있기 때문에 WAS 뿐만 아니라 DB 서버에도 영향이 갈 수도 있다.

image

포스트맨을 이용해 위의 메서드에 대한 응답속도를 테스트해보니 무려 약 5초나 걸리게 되었다.

image

이런 문제들을 해결 및 방지하고자 이메일 전송하는 로직을 비동기 처리를 도입하게 되었다.


@EnableAsync 어노테이션을 Application 클래스 위에 붙여 주고, 비동기 방식으로 처리하고 싶은 동기 로직의 메소드 위에 @Async 어노테이션을 붙여주면 기본적인 비동기 처리는 된다.

하지만 이 방식은 단점이 있다. @Async의 기본 설정은 SimpleAsyncTaskExecutor를 사용하도록 되어 있는데, 이것은 스레드 풀을 사용하는 방식이 아닌 새로운 스레드를 생성해서 실행시키기 때문이다.

image

5번의 요청을 했을 때, 새로운 스레드가 5개가 생기는 것을 확인할 수 있었다.

image


스레드풀 설정하기

스레드풀을 설정하기 위해서는 AsyncConfigurerSupport를 상속하거나 AsyncConfigurer인터페이스를 구현하면 된다.

AsyncConfigurerSupport도 AsyncConfigurer 인터페이스의 구현체다.

그리고 ThreadPoolTaskExecutor 객체를 만들어 스레드풀 설정 값들을 지정해주면 된다.

ThreadPoolTaskExecutor는 스프링이 제공해주는 클래스로, 내부적으로 Java의 ThreadPoolExecutor를 이용해 구현되어 있다.

image

  • corePoolSize: 기본적으로 실행 대기 중인 스레드 개수
  • maxPoolSize : 동시에 동작하는 최대 스레드 개수
  • queueCapacity : corePoolSize를 넘어설 때 태스크를 저장할 큐의 최대 용량

위의 설정한 상황을 예시로 들어보면, 2개의 스레드가 동작하고 있는 상황에서, 더 많은 요청이 들어온다면 태스크들을 큐에 저장시킨다. 요청이 계속 들어와 큐가 가득차게 된 상황에서도, 새로운 요청이 들어오면 스레드를 5개까지 만들어 실행시킨다.

5번의 요청을 했을 때, 2개의 스레드만 사용되고 있는 것을 확인할 수 있다.

image

톰캣의 스레드풀 수와 관련이 있나?

위에서 새로 정의한 스레드풀이 톰캣의 스레드풀에서 할당을 받는 것인지, 새로 생성을 하는 것인지 궁금해 톰캣의 maxThread를 1로 설정해보았다. 할당을 받는 구조라면 예외가 발생했을텐데, 2개의 비동기 스레드가 잘 동작되는 것을 확인할 수 있었다.


@Async 어노테이션의 동작 방식

@Async 어노테이션도 @Transactional 어노테이션과 같이 프록시 기반으로 동작하므로, @Async 매소드를 선언할 때 약간의 주의사항이 있다.

  • 호출하는 객체와 비동기 메서드가 다른 클래스여야 한다.
  • 스프링 프록시 특성 상, 같은 클래스에서 호출하게 되면 프록시 객체가 불러와지지 않기 때문이다.
  • private 메서드여서는 안된다.
  • 비동기 메서드를 가지고 있는 클래스에 final이 붙으면 안된다.

스프링 프록시는 기본값으로 CGLIB 방식을 사용하는데, CGLIB은 Target 객체를 상속하여 프록시 객체를 생성한다. 고로, final 클래스는 상속이 불가하여 프록시 객체를 만들 수 없고, private 메서드는 상속을 할 수 없기 때문이다.

비동기 메서드를 가지고 있는 mailSender가 처음 호출되는 시점을 디버깅해보았을 때 CGLIB으로 구현된 프록시 객체임을 확인할 수 있다.

image


결론적으로 이메일 전송하는 로직을 비동기적으로 동작하게 함으로써 응답시간을 5초에서 0.4초로 단축시킬 수 있었다.

image