20250113 TIL : 이메일 발송 비동기 처리하기

MCS·2025년 1월 13일

TIL

목록 보기
42/45

오늘 학습한 내용

  • 이메일 발송 비동기 처리하기
    • 이메일 발송은 기본적으로 동기로 처리된다
    • 이메일 발송 비동기로 처리하기(@Async)
    • 비동기 처리 시 주의할 점

이메일 발송 비동기 처리하기

최종 프로젝트에서 이메일 인증을 구현하고 사용했다. 여기서 겪었던 트러블슈팅 내용을 정리해보았다.

이메일 발송은 기본적으로 동기로 처리된다


인증 메일 발송 시 약 4~5초 정도의 응답 시간이 소요되고, 이메일 발송이 완료될 때 까지 대기하는 모습을 볼 수 있다.

이로 인해 다음과 같은 문제 상황이 생길 수 있다.

  • 트래픽이 많이 몰릴 경우 응답 시간이 더 느려질 수 있음
  • 요청이 완료될 때 까지 스레드가 점유되기 때문에 스레드 고갈 문제 발생 가능성 있음
  • 서비스 장애 및 사용자 경험 저하로 이어질 수 있음

문제 상황에 대해 분석해보았다.

현재 메일을 발송하는 코드는 다음과 같다.

@Slf4j
@Service
@RequiredArgsConstructor
public class MailServiceImpl implements MailService {
    private final JavaMailSender emailSender;

    public void sendEmail(String toEmail, String title, String text) {
        SimpleMailMessage emailForm = createEmailForm(toEmail, title, text);
        try {
            emailSender.send(emailForm);
        } catch (RuntimeException e) {
            log.debug("MailService.sendEmail exception occur toEmail: {}, " +
                    "title: {}, text: {}", toEmail, title, text);
            throw new IllegalArgumentException();
        }
    }

    // 발신할 이메일 데이터 세팅
    private SimpleMailMessage createEmailForm(String toEmail, String title, String text) {
        SimpleMailMessage message = new SimpleMailMessage();
        message.setTo(toEmail);
        message.setSubject(title);
        message.setText(text);

        return message;
    }
}

이메일 발송 비동기로 처리하기(@Async)

그렇다면 이메일 발송을 비동기로 처리하고, 이메일 발송과 상관없이 응답을 반환하도록 할 수 있을 것이다.
실제 서비스에서도 이메일이 발송되었다는 응답이 나오더라도 이메일이 발송될 때 까지 시간이 걸리는 경우가 종종 있다. 따라서 이메일이 발송되지 않더라도 일단 응답을 반환한다면 사용자 경험 개선에 도움이 될 것이다.

이를 위해 @Async 어노테이션을 사용해 메일 발송 메서드를 비동기 처리해보았다.
(비동기 처리를 하는 방법은 많겠지만, 이메일 인증 발송은 트래픽이 매우 크게 몰리는 경우도 잘 없을 것이므로 간단하게 처리해보았다.)

우선 메인 클래스에 @EnableAsync 어노테이션을 추가한다.

@SpringBootApplication
@EnableAsync
public class UserApplication {

	public static void main(String[] args) {
		SpringApplication.run(UserApplication.class, args);
	}

}

또는 Config 클래스를 만들어 관리할 수도 있다.

@Configuration
@EnableAsync
public class AsyncConfig {
    // 비동기 관련 추가 설정이 필요한 경우 추가
}

그리고 sendEmail 메서드에 @Async 어노테이션을 추가한다.

@Async
public void sendEmail(String toEmail, String title, String text) {
        SimpleMailMessage emailForm = createEmailForm(toEmail, title, text);
        try {
            emailSender.send(emailForm);
        } catch (RuntimeException e) {
            log.debug("MailService.sendEmail exception occur toEmail: {}, " +
                    "title: {}, text: {}", toEmail, title, text);
            throw new IllegalArgumentException();
        }
    }

비동기 처리 시 주의할 점

우선 @Async 어노테이션은 AOP를 통해 프록시로 동작하므로, 이 메서드는 외부 클래스에서 호출되어야 한다.
특정 메서드 내에서 MailServiceImpl을 new로 생성해 사용하거나, 같은 클래스 내부에서 메서드를 호출한다면 프록시 처리가 불가능해 어노테이션이 정확히 동작하지 않을 것이다. 이에 대한 내용은 이전에 정리했다.
(https://velog.io/@mcshin00/20241224-TIL-%ED%94%84%EB%A1%9D%EC%8B%9CProxy%EC%99%80-Spring-AOP)

현재 프로젝트에서는 MailServiceImpl은 infrastructure 계층에 존재하고, MailService 인터페이스는 application 계층에 존재한다. 또한 AuthService에서 MailService를 선언해 DI를 통해 MailServiceImpl를 호출하는 방식으로 되어 있다. 따라서 프록시 이슈는 없다.

그 외에, 비동기 메서드에서 발생하는 예외는 호출 측에 전파되지 않으므로, 예외 처리를 잘 관리해야 한다. 현재 코드에서는 RuntimeException을 잡아서 IllegalArgumentException을 던지고 있으므로 이 부분을 로그에 남기거나 전역 예외 처리로 관리하는 것이 좋다. 이후에 로깅을 추가할 예정이다.

또한 비동기 작업에서 트랜잭션이나 보안 컨텍스트와 같은 스레드 로컬 변수가 전파되지 않는다. 다만 sendEmail 메서드가 이러한 컨텍스트에 의존하지 않으므로 큰 문제가 없다.

마지막으로 비동기 메서드는 테스트가 동기 메서드에 비해 복잡할 수 있다. 비동기 작업이 완료될 때까지 기다리는 로직을 추가하거나, 테스트 프레임워크에서 지원하는 비동기 테스트 기능을 사용할 수 있다. 비동기의 테스트에 대해서는 추후 작성해 볼 예정이다.

profile
백엔드를 잘 하고 싶은 사람

0개의 댓글