diff --git a/src/main/java/com/daramg/server/auth/application/AsyncMailSender.java b/src/main/java/com/daramg/server/auth/application/AsyncMailSender.java new file mode 100644 index 0000000..34addb0 --- /dev/null +++ b/src/main/java/com/daramg/server/auth/application/AsyncMailSender.java @@ -0,0 +1,36 @@ +package com.daramg.server.auth.application; + +import com.daramg.server.auth.domain.MailMessages; +import com.daramg.server.auth.util.MailContentBuilder; +import com.daramg.server.auth.util.MimeMessageGenerator; +import jakarta.mail.internet.MimeMessage; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class AsyncMailSender { + + private final JavaMailSender javaMailSender; + private final MimeMessageGenerator mimeMessageGenerator; + private final MailContentBuilder mailContentBuilder; + + @Async("mailTaskExecutor") + public void sendVerificationCode(String email, String verificationCode) { + try { + String htmlContent = mailContentBuilder.buildVerificationEmail(verificationCode); + MimeMessage mimeMessage = mimeMessageGenerator.generate( + email, + MailMessages.MAIL_VERIFICATION_SUBJECT, + htmlContent + ); + javaMailSender.send(mimeMessage); + } catch (Exception e) { + log.error("이메일 발송 실패 - email: {}, error: {}", email, e.getMessage()); + } + } +} diff --git a/src/main/java/com/daramg/server/auth/application/MailVerificationServiceImpl.java b/src/main/java/com/daramg/server/auth/application/MailVerificationServiceImpl.java index 15b7752..468d35d 100644 --- a/src/main/java/com/daramg/server/auth/application/MailVerificationServiceImpl.java +++ b/src/main/java/com/daramg/server/auth/application/MailVerificationServiceImpl.java @@ -1,24 +1,18 @@ package com.daramg.server.auth.application; -import com.daramg.server.auth.domain.MailMessages; import com.daramg.server.auth.dto.EmailVerificationRequestDto; import com.daramg.server.auth.dto.CodeVerificationRequestDto; import com.daramg.server.auth.exception.AuthErrorStatus; import com.daramg.server.auth.repository.VerificationCodeRepository; -import com.daramg.server.auth.util.MailContentBuilder; -import com.daramg.server.auth.util.MimeMessageGenerator; import com.daramg.server.auth.util.VerificationCodeGenerator; import com.daramg.server.common.exception.BusinessException; import com.daramg.server.user.exception.UserErrorStatus; import com.daramg.server.user.repository.UserRepository; -import jakarta.mail.MessagingException; -import jakarta.mail.internet.MimeMessage; + import com.daramg.server.auth.repository.RateLimitRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.RedisConnectionFailureException; -import org.springframework.mail.MailException; -import org.springframework.mail.javamail.JavaMailSender; import org.springframework.stereotype.Service; @Slf4j @@ -26,9 +20,7 @@ @RequiredArgsConstructor public class MailVerificationServiceImpl implements MailVerificationService{ - private final MimeMessageGenerator mimeMessageGenerator; - private final MailContentBuilder mailContentBuilder; - private final JavaMailSender javaMailSender; + private final AsyncMailSender asyncMailSender; private final VerificationCodeRepository verificationCodeRepository; private final RateLimitRepository rateLimitRepository; private final UserRepository userRepository; @@ -77,19 +69,7 @@ private void sendVerificationCode(EmailVerificationRequestDto request) { verificationCodeRepository.save(request.getEmail(), verificationCode) ); - try { - String htmlContent = mailContentBuilder.buildVerificationEmail(verificationCode); - MimeMessage mimeMessage = mimeMessageGenerator.generate( - request.getEmail(), - MailMessages.MAIL_VERIFICATION_SUBJECT, - htmlContent - ); - - javaMailSender.send(mimeMessage); - } catch (MessagingException | MailException | java.io.UnsupportedEncodingException e) { - log.error("이메일 발송 실패 - email: {}, error: {}", request.getEmail(), e.getMessage()); - throw new BusinessException(AuthErrorStatus.SEND_VERIFICATION_EMAIL_FAILED); - } + asyncMailSender.sendVerificationCode(request.getEmail(), verificationCode); } public void verifyEmailWithCode(CodeVerificationRequestDto request) { diff --git a/src/main/java/com/daramg/server/common/config/AsyncConfig.java b/src/main/java/com/daramg/server/common/config/AsyncConfig.java new file mode 100644 index 0000000..eb99f92 --- /dev/null +++ b/src/main/java/com/daramg/server/common/config/AsyncConfig.java @@ -0,0 +1,24 @@ +package com.daramg.server.common.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.Executor; + +@Configuration +@EnableAsync +public class AsyncConfig { + + @Bean(name = "mailTaskExecutor") + public Executor mailTaskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(2); + executor.setMaxPoolSize(5); + executor.setQueueCapacity(100); + executor.setThreadNamePrefix("mail-"); + executor.initialize(); + return executor; + } +} diff --git a/src/test/java/com/daramg/server/auth/application/MailVerificationServiceImplTest.java b/src/test/java/com/daramg/server/auth/application/MailVerificationServiceImplTest.java index b3bd52c..efae6c7 100644 --- a/src/test/java/com/daramg/server/auth/application/MailVerificationServiceImplTest.java +++ b/src/test/java/com/daramg/server/auth/application/MailVerificationServiceImplTest.java @@ -1,15 +1,13 @@ package com.daramg.server.auth.application; import com.daramg.server.auth.domain.EmailPurpose; +import com.daramg.server.auth.dto.CodeVerificationRequestDto; import com.daramg.server.auth.dto.EmailVerificationRequestDto; import com.daramg.server.auth.exception.AuthErrorStatus; import com.daramg.server.auth.repository.RateLimitRepository; import com.daramg.server.auth.repository.VerificationCodeRepository; -import com.daramg.server.auth.util.MailContentBuilder; -import com.daramg.server.auth.util.MimeMessageGenerator; import com.daramg.server.common.exception.BusinessException; import com.daramg.server.user.repository.UserRepository; -import jakarta.mail.internet.MimeMessage; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -17,12 +15,10 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.mail.javamail.JavaMailSender; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.*; @@ -30,9 +26,7 @@ @ExtendWith(MockitoExtension.class) class MailVerificationServiceImplTest { - @Mock private MimeMessageGenerator mimeMessageGenerator; - @Mock private MailContentBuilder mailContentBuilder; - @Mock private JavaMailSender javaMailSender; + @Mock private AsyncMailSender asyncMailSender; @Mock private VerificationCodeRepository verificationCodeRepository; @Mock private RateLimitRepository rateLimitRepository; @Mock private UserRepository userRepository; @@ -48,16 +42,14 @@ class SendVerificationEmail { @Test @DisplayName("새 코드 발급 시 시도 횟수를 초기화하지 않는다") - void 새_코드_발급_시_시도_횟수_초기화_안됨() throws Exception { + void 새_코드_발급_시_시도_횟수_초기화_안됨() { given(userRepository.existsByEmail(TEST_EMAIL)).willReturn(false); given(rateLimitRepository.isRateLimited(TEST_EMAIL)).willReturn(false); - given(mailContentBuilder.buildVerificationEmail(anyString())).willReturn("code"); - given(mimeMessageGenerator.generate(anyString(), anyString(), anyString())) - .willReturn(mock(MimeMessage.class)); EmailVerificationRequestDto request = new EmailVerificationRequestDto(null, TEST_EMAIL, EmailPurpose.SIGNUP); mailVerificationService.sendVerificationEmail(request); + verify(asyncMailSender).sendVerificationCode(eq(TEST_EMAIL), anyString()); verify(rateLimitRepository, never()).resetAttempts(TEST_EMAIL); } @@ -72,6 +64,8 @@ class SendVerificationEmail { assertThatThrownBy(() -> mailVerificationService.sendVerificationEmail(request)) .isInstanceOf(BusinessException.class) .hasFieldOrPropertyWithValue("errorCode", AuthErrorStatus.EMAIL_RATE_LIMIT_EXCEEDED); + + verify(asyncMailSender, never()).sendVerificationCode(anyString(), anyString()); } @Test @@ -84,6 +78,8 @@ class SendVerificationEmail { assertThatThrownBy(() -> mailVerificationService.sendVerificationEmail(request)) .isInstanceOf(BusinessException.class) .hasFieldOrPropertyWithValue("errorCode", AuthErrorStatus.DUPLICATE_EMAIL); + + verify(asyncMailSender, never()).sendVerificationCode(anyString(), anyString()); } @Test @@ -96,6 +92,8 @@ class SendVerificationEmail { assertThatThrownBy(() -> mailVerificationService.sendVerificationEmail(request)) .isInstanceOf(BusinessException.class) .hasFieldOrPropertyWithValue("errorCode", AuthErrorStatus.EMAIL_NOT_REGISTERED); + + verify(asyncMailSender, never()).sendVerificationCode(anyString(), anyString()); } } @@ -108,11 +106,8 @@ class VerifyEmailWithCode { void 인증_성공_시_시도_횟수_초기화() { given(rateLimitRepository.isAttemptExceeded(TEST_EMAIL)).willReturn(false); given(verificationCodeRepository.findByEmail(TEST_EMAIL)).willReturn(Optional.of("123456")); - doNothing().when(verificationCodeRepository).deleteByEmail(TEST_EMAIL); - doNothing().when(rateLimitRepository).resetAttempts(TEST_EMAIL); - com.daramg.server.auth.dto.CodeVerificationRequestDto request = - new com.daramg.server.auth.dto.CodeVerificationRequestDto(TEST_EMAIL, "123456"); + CodeVerificationRequestDto request = new CodeVerificationRequestDto(TEST_EMAIL, "123456"); mailVerificationService.verifyEmailWithCode(request); verify(rateLimitRepository).resetAttempts(TEST_EMAIL); @@ -123,8 +118,7 @@ class VerifyEmailWithCode { void 시도_횟수_초과_시_예외_발생() { given(rateLimitRepository.isAttemptExceeded(TEST_EMAIL)).willReturn(true); - com.daramg.server.auth.dto.CodeVerificationRequestDto request = - new com.daramg.server.auth.dto.CodeVerificationRequestDto(TEST_EMAIL, "123456"); + CodeVerificationRequestDto request = new CodeVerificationRequestDto(TEST_EMAIL, "123456"); assertThatThrownBy(() -> mailVerificationService.verifyEmailWithCode(request)) .isInstanceOf(BusinessException.class) @@ -137,8 +131,7 @@ class VerifyEmailWithCode { given(rateLimitRepository.isAttemptExceeded(TEST_EMAIL)).willReturn(false); given(verificationCodeRepository.findByEmail(TEST_EMAIL)).willReturn(Optional.of("123456")); - com.daramg.server.auth.dto.CodeVerificationRequestDto request = - new com.daramg.server.auth.dto.CodeVerificationRequestDto(TEST_EMAIL, "999999"); + CodeVerificationRequestDto request = new CodeVerificationRequestDto(TEST_EMAIL, "999999"); assertThatThrownBy(() -> mailVerificationService.verifyEmailWithCode(request)) .isInstanceOf(BusinessException.class)