회원가입을 할 때 이메일을 통해서 인증을 하고 있다. 하지만 SMTP는 외부서비스이며 실제로 굉장히 느리다.
처음 브라우저에서 버튼을 눌렀을 때 2~3 초동안 반응이 없었고 짧다면 짧은 순간이지만 고객의 입장에서는 충분히 불편할 수 있는 순간이었다.
그래서 1차적으로 낸 해결책은 최소한의 반응을 주는 것이었다. 프론트 화면에서 버튼을 누르고 나면 위와 같이 버튼이 로딩중
으로 바뀌어서, 고객의 요청에 처리중이라는 것을 알려주었다.
어느정도 불편함은 해속되었지만 이메일 인증을 하는데 3~4초가 걸리는 문제를 서버 내부적으로 해결해야할 필요성을 느꼈다.
이메일을 보내는 것은 외부서비스이다. 우리 서버에서는 외부서비스에 이메일을 보내달라는 요청을 보내면 그것으로 할일은 끝난 것이라고 생각을 했다. 외부서비스의 문제가 있다고 하더라도 우리 서버측에서 현재 대응할 수 있는 방법은 없다. 그렇다면 이메일을 보내는 과정 자체를 쓰레드가 기다리는 것이 아니라 요청을 보내고 쓰레드는 다른 일을 해도 될 것 같다.
비동기를 사용하기 위해서 설정한 코드는 다음과 같다.
executor.setCorePoolSize(2)
: 기본적으로 실행 대기 중인 Thread 의 개수executor.setMaxPoolSize(10)
: 동시에 동작하는 최대 Thread의 개수executor.setQueueCapacity(500)
: CorePool
의 크기를 넘어서면 큐에 저장하는데 그 큐의 최대 용량이다. 디폴트는
SimpleAsyncTaskExecutor
이지만 Executor 타입의 빈이 한개라면 그 빈으로 돌아간다. 여러 개의 빈으로 등록한다면SimpleAsyncTaskExecutor
가 돌아가므로 이름을 통해 명시해주어야 한다.
그리고 위의 설정을 바탕으로 실제로 비동기를 적용하는 코드는 아래와 같다.
기존 코드에서 @Async
어노테이션 하나만 붙여주었다. 이렇게 되면 한 스레드에서는 성공 응답 값을 보내주고 다른 스레드에서는 이메일을 전송하고 있을 것이다.
기존에 3.22s 걸리던 동작이 8ms 밖에 걸리지 않는 것을 확인할 수 있었다.
@EnableAsync
어노테이션을 사용하면 기본적으로 SimpleAsyncTaskExecutor
를 사용한다. SimpleAsyncTaskExecutor
는 매번 쓰레드를 생성하는 방식이기 때문에 오버라이딩해서 쓰레드 풀을 이용하도록 해야 한다.
기본 스레드풀로 SimpleAsyncTaskExecutor
풀을 사용하는데 따로 설정을 해두면 설정한 스레드풀을 사용한다. 스레드풀을 찾으면 Job을 스레드풀에 넘겨준다.
스프링 내부에서는 AOP로 비동기 처리를 수행하고 있다. 따라서 트랜잭션과 마찬가지로 2가지 제약조건이 있다.
프록시를 사용하기 위해
리턴 타입이 Void인 경우 예외가 발생해도 메서드 호출 쓰레드까지 전파되지 않아서 기존의 ControllerAdvice
에서 처리할 수 없다. 그래서 AsyncUncaughtExceptionHandler
를 구현한 클래스를 생성하고 AsyncConfigurer
인터페이스의 getAsyncUncaughtExceptionHandler
를 오버라이딩 해야한다.
현재 서비스에서는 비동기로 이메일 발송 요청을 하고 사용자에게는 이미 204를 보낸다. 그리고 SMTP 서비스가 장애가 발생해도 사실 크게 대응해줄 수 있는 방법이 없다. 그래서 예외처리가 의미가 있나 싶다. 다른 서비스들에서도 생각해보면 '5분이 지나서 이메일이 오지 않으면 다시 눌러주세요' 같은 문구가 있으니 서버단에서 적극적으로 처리를 해줄 수는 없는 것 같다.
제이슨과 얘기해본 결과 실제로 적극적 대응 처리를 할 수는 없는 것 같다. 대응을 하려면 이메일을 성공적으로 보낸 이메일과 못보낸 이메일을 저장하고 관리자 페이지에서 수동으로 보내줄 수 있다. 이렇게 운영으로 풀어야 한다.
찾아보다보니 이벤트 발행이 비동기에 꼭 연관되어 나오는 것을 봤다. 이 부분은 나중에 필요하면 학습할 예정이다.
토르 이건 저도 고민인데, 여기 프로덕트에선 이메일 인증이 되게 중요한 요소 같은데, 만약 transaction은 commit되고 인증코드가 비동기로 발행 실패된다면 어떻게 할 생각이신가요??