Skip to content

Commit 2906fdd

Browse files
authored
Gmail SMTP에서 AWS SES SDK v2로 이메일 발송 인프라 교체 (#81)
* chore: Gmail SMTP에서 AWS SES SDK v2로 이메일 발송 인프라 교체 * refactor: 이메일 전송 예외 처리 및 테스트 보완
1 parent 276e7a6 commit 2906fdd

5 files changed

Lines changed: 86 additions & 43 deletions

File tree

build.gradle

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,9 @@ repositories {
3030
dependencies {
3131
implementation 'org.springframework.boot:spring-boot-starter-web'
3232
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
33-
implementation 'org.springframework.boot:spring-boot-starter-mail'
3433
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
34+
implementation platform('software.amazon.awssdk:bom:2.29.52')
35+
implementation 'software.amazon.awssdk:sesv2'
3536
runtimeOnly 'com.mysql:mysql-connector-j'
3637
compileOnly 'org.projectlombok:lombok'
3738
annotationProcessor 'org.projectlombok:lombok'
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.recyclestudy.common.config;
2+
3+
import org.springframework.context.annotation.Bean;
4+
import org.springframework.context.annotation.Configuration;
5+
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
6+
import software.amazon.awssdk.regions.Region;
7+
import software.amazon.awssdk.services.sesv2.SesV2Client;
8+
9+
@Configuration
10+
public class AwsSesConfig {
11+
12+
@Bean
13+
public SesV2Client sesV2Client() {
14+
return SesV2Client.builder()
15+
.region(Region.AP_NORTHEAST_2)
16+
.credentialsProvider(DefaultCredentialsProvider.create())
17+
.build();
18+
}
19+
}

src/main/java/com/recyclestudy/email/EmailSender.java

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,46 @@
22

33
import com.recyclestudy.exception.EmailSendException;
44
import com.recyclestudy.member.domain.Email;
5-
import jakarta.mail.MessagingException;
6-
import jakarta.mail.internet.MimeMessage;
75
import lombok.RequiredArgsConstructor;
86
import lombok.extern.slf4j.Slf4j;
9-
import org.springframework.mail.javamail.JavaMailSender;
10-
import org.springframework.mail.javamail.MimeMessageHelper;
117
import org.springframework.stereotype.Component;
8+
import software.amazon.awssdk.core.exception.SdkException;
9+
import software.amazon.awssdk.services.sesv2.SesV2Client;
10+
import software.amazon.awssdk.services.sesv2.model.Body;
11+
import software.amazon.awssdk.services.sesv2.model.Content;
12+
import software.amazon.awssdk.services.sesv2.model.Destination;
13+
import software.amazon.awssdk.services.sesv2.model.EmailContent;
14+
import software.amazon.awssdk.services.sesv2.model.Message;
15+
import software.amazon.awssdk.services.sesv2.model.SendEmailRequest;
1216

1317
@Slf4j
1418
@Component
1519
@RequiredArgsConstructor
1620
public class EmailSender {
1721

18-
private final JavaMailSender javaMailSender;
22+
private static final String FROM_ADDRESS = "noreply@recycle-study.site";
23+
24+
private final SesV2Client sesV2Client;
1925

2026
public void send(final Email targetEmail, final String subject, final String content) {
27+
final SendEmailRequest request = SendEmailRequest.builder()
28+
.fromEmailAddress(FROM_ADDRESS)
29+
.destination(Destination.builder()
30+
.toAddresses(targetEmail.getValue())
31+
.build())
32+
.content(EmailContent.builder()
33+
.simple(Message.builder()
34+
.subject(Content.builder().data(subject).charset("UTF-8").build())
35+
.body(Body.builder()
36+
.html(Content.builder().data(content).charset("UTF-8").build())
37+
.build())
38+
.build())
39+
.build())
40+
.build();
2141
try {
22-
final MimeMessage mimeMessage = javaMailSender.createMimeMessage();
23-
final MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, false, "UTF-8");
24-
25-
helper.setTo(targetEmail.getValue());
26-
helper.setSubject(subject);
27-
helper.setText(content, true);
28-
29-
javaMailSender.send(mimeMessage);
42+
sesV2Client.sendEmail(request);
3043
log.info("[MAIL_SENT] 메일 발송 성공: email={}", targetEmail.toMaskedValue());
31-
} catch (MessagingException e) {
44+
} catch (final SdkException e) {
3245
log.error("[MAIL_SEND_FAILED] 메일 발송 실패: email={}", targetEmail.toMaskedValue(), e);
3346
throw new EmailSendException("메일 전송 중 오류가 발생했습니다.", e);
3447
}

src/main/resources/application.yaml

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,6 @@ spring:
2727

2828
open-in-view: false
2929

30-
mail:
31-
host: smtp.gmail.com
32-
port: 587
33-
username: ${GMAIL_USERNAME}
34-
password: ${GMAIL_PASSWORD}
35-
properties:
36-
mail:
37-
smtp:
38-
starttls:
39-
enable: true
40-
auth: true
4130

4231
server:
4332
tomcat:

src/test/java/com/recyclestudy/email/EmailSenderTest.java

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,29 @@
22

33
import com.recyclestudy.exception.EmailSendException;
44
import com.recyclestudy.member.domain.Email;
5-
import jakarta.mail.MessagingException;
6-
import jakarta.mail.internet.MimeMessage;
5+
import org.assertj.core.api.SoftAssertions;
76
import org.junit.jupiter.api.DisplayName;
87
import org.junit.jupiter.api.Test;
98
import org.junit.jupiter.api.extension.ExtendWith;
9+
import org.mockito.ArgumentCaptor;
1010
import org.mockito.InjectMocks;
1111
import org.mockito.Mock;
1212
import org.mockito.junit.jupiter.MockitoExtension;
13-
import org.springframework.mail.javamail.JavaMailSender;
13+
import software.amazon.awssdk.core.exception.SdkClientException;
14+
import software.amazon.awssdk.services.sesv2.SesV2Client;
15+
import software.amazon.awssdk.services.sesv2.model.SendEmailRequest;
16+
import software.amazon.awssdk.services.sesv2.model.SesV2Exception;
1417

1518
import static org.assertj.core.api.Assertions.assertThatThrownBy;
1619
import static org.mockito.ArgumentMatchers.any;
17-
import static org.mockito.BDDMockito.given;
1820
import static org.mockito.BDDMockito.willThrow;
19-
import static org.mockito.Mockito.mock;
2021
import static org.mockito.Mockito.verify;
2122

2223
@ExtendWith(MockitoExtension.class)
2324
class EmailSenderTest {
2425

2526
@Mock
26-
private JavaMailSender javaMailSender;
27+
private SesV2Client sesV2Client;
2728

2829
@InjectMocks
2930
private EmailSender emailSender;
@@ -35,32 +36,52 @@ void send_success() {
3536
final Email targetEmail = Email.from("test@test.com");
3637
final String subject = "테스트 제목";
3738
final String content = "<html>테스트 내용</html>";
38-
final MimeMessage mimeMessage = mock(MimeMessage.class);
39-
40-
given(javaMailSender.createMimeMessage()).willReturn(mimeMessage);
4139

4240
// when
4341
emailSender.send(targetEmail, subject, content);
4442

4543
// then
46-
verify(javaMailSender).createMimeMessage();
47-
verify(javaMailSender).send(mimeMessage);
44+
ArgumentCaptor<SendEmailRequest> captor = ArgumentCaptor.forClass(SendEmailRequest.class);
45+
verify(sesV2Client).sendEmail(captor.capture());
46+
SendEmailRequest request = captor.getValue();
47+
SoftAssertions.assertSoftly(softly -> {
48+
softly.assertThat(request.fromEmailAddress()).isEqualTo("noreply@recycle-study.site");
49+
softly.assertThat(request.destination().toAddresses()).containsExactly("test@test.com");
50+
softly.assertThat(request.content().simple().subject().data()).isEqualTo(subject);
51+
});
4852
}
4953

5054
@Test
5155
@DisplayName("메일 발송 실패 시 EmailSendException을 던진다")
52-
void send_fail_throwsException() throws MessagingException {
56+
void send_fail_throwsException() {
5357
// given
5458
final Email targetEmail = Email.from("test@test.com");
5559
final String subject = "테스트 제목";
5660
final String content = "<html>테스트 내용</html>";
57-
final MimeMessage mimeMessage = mock(MimeMessage.class);
5861

59-
given(javaMailSender.createMimeMessage()).willReturn(mimeMessage);
60-
willThrow(new MessagingException("메일 서버 오류"))
61-
.given(mimeMessage).setRecipient(any(), any());
62+
willThrow(SesV2Exception.builder().message("SES 오류").build())
63+
.given(sesV2Client).sendEmail(any(SendEmailRequest.class));
6264

63-
// when & then
65+
// when
66+
// then
67+
assertThatThrownBy(() -> emailSender.send(targetEmail, subject, content))
68+
.isInstanceOf(EmailSendException.class)
69+
.hasMessage("메일 전송 중 오류가 발생했습니다.");
70+
}
71+
72+
@Test
73+
@DisplayName("네트워크 오류 시 EmailSendException을 던진다")
74+
void send_fail_networkError_throwsEmailSendException() {
75+
// given
76+
final Email targetEmail = Email.from("test@test.com");
77+
final String subject = "테스트 제목";
78+
final String content = "<html>테스트 내용</html>";
79+
80+
willThrow(SdkClientException.create("네트워크 오류"))
81+
.given(sesV2Client).sendEmail(any(SendEmailRequest.class));
82+
83+
// when
84+
// then
6485
assertThatThrownBy(() -> emailSender.send(targetEmail, subject, content))
6586
.isInstanceOf(EmailSendException.class)
6687
.hasMessage("메일 전송 중 오류가 발생했습니다.");

0 commit comments

Comments
 (0)