[SpringBoot] AWS SES로 이메일 전송 기능 구현하기
이번 포스팅에서는 AWS SES 환경을 구축하고 Java SpringBoot에서 HTML 템플릿 기반의 이메일을 전송하는 방법에 대해 알아보도록 하겠습니다.
AWS SES 란?
AWS`Amazon Web Service`에서 제공하는 SES`Simple Email Service`는 클라우드 기반의 이메일 발송 서비스로 Email 전송 서비스를 이용할 수 있는 라이브러리입니다.
AWS 환경 구축
AWS 사이트에 접속 후 로그인 한 뒤 `우측 상단`에 위치한 리전을 서울로 설정합니다.
`좌측 상단` 검색창을 통해 `ses`를 검색하여 `Amazon Simple Email Service`를 선택합니다.
이후 좌측 `자격 증명`탭을 선택하여 `자격 증명 생성`을 선택하여 자격 증명 생성을 시작합니다.
`이메일 주소` 선택 → `본인 이메일 주소` 입력 → `자격 증명 생성` 클릭
`자격 증명 생성` 후 AWS로부터 인증 메일을 받습니다. 해당 링크를 클릭하게 되면 메일 인증에 성공하게 됩니다.
이메일 인증 전 후 `보안 인증 상태`입니다. `확인됨`이 표시된다면 메일 인증이 완료된 것입니다.
테스트 이메일 전송
AWS SES에서 생성된 자격 증명이 정상적으로 작동하는지 확인하려면 `테스트 이메일 전송` 기능을 사용해 볼 수 있습니다.
생성한 자격증명을 선택하여 `테스트 이메일 전송`을 클릭합니다.
`형식 지정` → `사용자 지정` → `수신자 이메일`, `제목`, `본문`을 각각 입력합니다.
- 주의사항
- `발신자 주소` 발신자 주소는 생성한 자격 증명의 이메일 주소로 자동 설정됩니다.
- `SANDBOX 제한` 처음 생성된 AWS SES는 기본적으로 `SANDBOX` 상태입니다. 이 상태에서는 검증된 이메일 주소(자격증명 생성된 것)로만 발송할 수 있습니다. SANDBOX를 해제하려면 AWS에 추가 요청을 보내야 합니다.
테스트 이메일이 정상적으로 전송된 것을 볼 수 있습니다.
간혹 네트워크 환경에 따라 이메일 발송이 지연될 수 있습니다.
IAM 설정
애플리케이션에서 AWS의 리소스 접근을 위해 IAM 사용자를 생성하여 권한을 부여합니다.
`좌측 상단`의 검색창을 통해 `IAM`을 검색하여 해당 서비스로 이동합니다.
`사용자`탭 선택 → `사용자 생성` 클릭
사용자 이름을 입력하고 `다음`을 클릭합니다.
`직접 정책 연결` 선택 → `AmazonSESFullAccess` 검색하여 권한 정책 추가 → 다음
권한 요약에서 추가한 권한 정책을 확인하고 `사용자 생성`을 클릭해 마무리합니다.
생성된 사용자를 선택하여 `액세스 키 만들기`를 클릭합니다.
본인이 해당되는 사용 사례를 선택하여 `다음`을 클릭합니다.
설명 태그 값을 입력한 뒤 `액세스 키 만들기`를 클릭하여 마무리합니다.
표시된 액세스 키`access key`, 비밀 액세스 키`secret access key`를 따로 메모하여 저장하거나 `.csv`파일을 다운로드하여 보관합니다.
해당 페이지를 벗어나면 이후 액세스 키를 확인할 수 없어 새로 생성해야하니 잘 저장해두어야 합니다.
Spring Boot SES 적용하기
AWS 환경 구축을 마치고 Spring Boot에서 이를 적용합니다.
의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' // 이메일을 템플릿으로 전송
implementation 'software.amazon.awssdk:ses:2.29.46' // AWS SDK
필자의 경우 AWS SDK 2.x 버전을 사용하였습니다.
SES 이메일 전송 기능 구현에 대한 글을 찾아보다 보면 AWS SDK 1.x 버전을 사용하는 경우도 많이 보이는데, 1.x 버전과 2.x 버전의 차이는 다음과 같습니다.
- 1.x.x 버전
- 클라이언트 클래스 `AmazonSimpleEmailService`
- Java 8 이전의 코딩 스타일을 따릅니다.
- 개별 서비스별로 경량화된 의존성을 제공하지 않으며, 모든 AWS 서비스를 포함한 거대한 단일 라이브러리 `aws-java-adk`로 제공됩니다.
// 1.x.x 예시 코드
AmazonSimpleEmailService client = AmazonSimpleEmailServiceClientBuilder.standard()
.withRegion(Regions.AP_NORTHEAST_2)
.build();
SendEmailRequest request = new SendEmailRequest()
.withSource("example@example.com")
.withDestination(new Destination().withToAddresses("recipient@example.com"))
.withMessage(new Message()
.withSubject(new Content().withData("Test Subject"))
.withBody(new Body().withText(new Content().withData("Test Body"))));
client.sendEmail(request);
- 2.x.x 버전
- 클라이언트 클래스 `SesClient`
- Java(8이상)의 기능을 활용해 더 직관적이고 간결한 코드를 제공합니다.
- SES 등 AWS 서비스를 개별 모듈로 분리해 제공하여, 필요한 서비스만 의존성에 추가할 수 있습니다. `software.amazon.awssdk:ses` (SES에만 필요한 종속성)
// 2.x.x 예시 코드
SesClient sesClient = SesClient.builder()
.region(Region.AP_NORTHEAST_2)
.build();
SendEmailRequest request = SendEmailRequest.builder()
.source("example@example.com")
.destination(Destination.builder().toAddresses("recipient@example.com").build())
.message(Message.builder()
.subject(Content.builder().data("Test Subject").build())
.body(Body.builder().text(Content.builder().data("Test Body").build()).build())
.build())
.build();
sesClient.sendEmail(request);
application.yml 설정
aws:
ses:
access-key: ${AWS_ACCESS_KEY} # IAM 액세스 키
secret-key: ${AWS_SECRET_KEY} # IAM 시크릿 키
send-mail-from: ${ADMIN_EMAIL} # 발신자 이메일 설정
region: ${AWS_REGION} # 지역 설정
IAM 사용자 생성 후 발급받은 access, secret key와 region`ap-northeast-2` (서울)을 입력합니다.
필자의 경우 발신자 이메일을 하나로 설정하기 위해 따로 발신자 이메일을 설정하였습니다.
SES Config 작성
@Configuration
public class SesConfig {
@Value("${aws.ses.access-key}")
private String accessKey;
@Value("${aws.ses.secret-key}")
private String secretKey;
@Value("${aws.region}")
private String region;
@Bean
public SesClient amazonSimpleEmailService() {
AwsBasicCredentials awsBasicCredentials = AwsBasicCredentials.create(accessKey, secretKey);
return SesClient.builder()
.region(Region.of(region))
.credentialsProvider(StaticCredentialsProvider.create(awsBasicCredentials))
.build();
}
}
application.yml에서 작성한 내용을 `@Value`를 통해 주입받아 `@Bean`을 생성합니다.
SES 요구사항 객체 생성
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class MailInfo {
private String from;
private List<String> to;
private String subject;
private String content;
@Builder
public MailInfo(String from, List<String> to, String subject, String content) {
this.from = from;
this.to = to;
this.subject = subject;
this.content = content;
}
public SendEmailRequest toSendEmailRequest() {
Destination destination = Destination.builder()
.toAddresses(this.to)
.build();
Message message = Message.builder()
.subject(createContent(this.subject))
.body(Body.builder().html(createContent(this.content)).build())
.build();
return SendEmailRequest.builder()
.source(this.from)
.destination(destination)
.message(message)
.build();
}
private Content createContent(String text) {
return Content.builder()
.charset("UTF-8")
.data(text)
.build();
}
}
메일 전송 시 SES에서 요구하는 사항에 맞춰 작성해주어야하는데, 재사용성을 위해 이를 객체로 생성하였습니다.
수신자는 여러명이 될 수 있기에 List로 생성하였습니다.
`toSendEmailRequest`
해당 메서드는 `MailInfo` 객체를 `SendEmailRequest` 객체로 변환하는 역할을 합니다. `SendEmailRequest`는 AWS SES에서 이메일을 전송하기 위한 요청 객체입니다.
`createContent`
해당 메서드는 텍스트를 `Content` 객체로 변환합니다. Content 객체는 이메일을 제목과 본문을 나타냅니다.
`charset("UTF-8")` 문자 인코딩을 UTF-8로 설정합니다.
`data(text)` 이메일의 내용을 설정합니다.
Template Config 작성
@Configuration
public class TemplateConfig {
@Bean
public TemplateEngine htmlTemplateEngine(SpringResourceTemplateResolver springResourceTemplateResolver) {
TemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.addTemplateResolver(springResourceTemplateResolver);
return templateEngine;
}
}
스프링 컨테이너에 `TemplateEngine` 객체를 Bean으로 등록합니다. 등록된 Bean은 스프링 애플리케이션 내에서 주입되어 사용할 수 있습니다.
이메일 발송 템플릿 작성
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>테스트 이메일 제목</title>
</head>
<body>
<h1>🎉 테스트 이메일 소제목</h1>
<p>테스트 이메일 본문</p>
</body>
</html>
작성한 HTML템플릿을 `./resources/templates` 경로에 추가합니다.
Service
@Service
@RequiredArgsConstructor
public class TestEmailService {
@Value("${aws.ses.send-mail-from}")
private String sender;
private final SesClient sesClient;
private final TemplateEngine templateEngine;
public void send() {
Context context = new Context();
// 템플릿을 렌더링하여 HTML 콘텐츠 생성
String content = templateEngine.process("reminder-email", context);
// 이메일 발송 객체 생성
EmailInfo emailInfo = EmailInfo.builder()
.from(sender)
.to(Collections.singletonList("발송할 이메일 입력"))
.subject("테스트 이메일 제목")
.content(content)
.build();
// 이메일 발송
sesClient.sendEmail(emailInfo.toSendEmailRequest());
}
}
`AWS SES`를 통해 이메일을 발송하는 Service 클래스 입니다. `Thymeleaf`를 사용해 이메일 본문을 HTML 템플릿 기반으로 생성하고, AWS SES 클라이언트를 통해 이메일을 발송하는 역할을 합니다.
이메일 발송을 위한 Controller
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/mail")
public class EmailController {
private final TestEmailService testEmailService;
@PostMapping
public ResponseEntity<ApiResponse<String>> sendReminder() {
testEmailService.send();
return ApiResponse.success(MAIL_SEND_SUCCESS);
}
}
이메일 발송 테스트를 위한 Controller를 생성하였습니다.
해당 API를 실행하면 정상적으로 이메일이 전송되는 모습을 볼 수 있습니다.
Thymeleaf문을 사용하여 템플릿 데이터를 동적 표현
UserEntity user = userRepository.findById(1L).orElseThrow();
Context context = new Context();
context.setVariable("nickName", user.getNickName());
String content = templateEngine.process("reminder-email", context);
조회 쿼리와 `context.setVariable`을 통해 데이터를 동적으로 표현이 가능합니다.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="ko">
<head>
<meta charset="UTF-8">
<title>테스트 이메일 제목</title>
</head>
<body>
<h1>🎉 안녕하세요, <span th:text="${nickName}"></span>님!</h1>
<p>테스트 이메일 본문</p>
</body>
</html>
Service에서 전달한 데이터를 Thymeleaf문을 통해 출력합니다.
마무리
이번 포스팅에서는 AWS의 SES`Simple Email Service`를 통해 이메일을 발송하기 위해 AWS 인프라 구축과 `Java Spring Boot`환경에서 이메일 전송 API를 구현하는 예제를 통해 미리 작성해 놓은 HTML형식의 템플릿을 발송하는 방법에 대해 알아보았습니다.
필자의 경우 Thymeleaf를 사용하여 이메일 템플릿의 메세지를 동적으로 표현하였는데 이 밖에도 `FreeMarker`, `Mustache` 등 여러가지 템플릿 엔진이 존재하며, 각 상황에 맞추어 적절한 방법을 사용하여 템플릿을 이용한 이메일 전송이 가능합니다.
'Spring' 카테고리의 다른 글
[Spring Boot] @Scheduled를 이용한 스케줄러 구현 (0) | 2025.01.13 |
---|---|
[SpringBoot] 다양한 동시성 제어 방법 (2) | 2024.12.20 |
[SpringBoot] Prometheus, Grafana를 이용한 모니터링 (0) | 2024.12.10 |
[Spring] Redis를 사용한Session 로그인 구현, Security없이 인증, 인가 구현 (26) | 2024.11.14 |
[Spring] offset, no offset 차이점과 페이지네이션 구현예제 (6) | 2024.10.23 |