들어가기
spring boot를 통해 프로젝트를 진행하던 중 초대장을 메일로 전송하는 기능을 구현해야 하여 메일 전송에 대해서 알아보고자 한다.
일반 웹 통신(HTTP)와 다르게 메일 전송을 위해서는 SMTP를 사용한다.
SMTP(Simple Mail Transfer Protocol)는 이메일을 전송하기 위해 사용되는 인터넷 표준 프로토콜이며, SMTP는 이메일 메시지를 작성한 후, 해당 메시지를 수신자에게 전달하는 과정에서 주로 사용된다. 이 프로토콜은 발신자의 이메일 서버에서 수신자의 이메일 서버로 메시지를 전송하는 역할을 한다.
아래의 사진을 봐보자.
1. 발신자는 자신의 이메일 클라이언트에서 이메일 메시지를 작성하고, 수신자의 이메일 주소를 입력한 후 메시지를 전송한다.
2. 메시지(및 첨부 파일)는 SMTP를 통해 발신 SMTP 서버로 전송된다.
3. SMTP 서버는 도메인 이름 서버(DNS)를 통해 수신자의 도메인을 조회하고, 수신자의 이메일 교환 서버(Mail Exchange Server)의 물리적 주소를 식별하려고 시도한다.
4. SMTP 서버는 메시지(및 첨부 파일)를 SMTP를 통해 수신자의 수신 메일 교환 서버 또는 메일 전송 에이전트(MTA)로 전송한다.
5. MTA는 메시지를 수신자의 받은 편지함에 넣는다.
6. 수신자는 메시지에 접근하고, 이를 자신의 로컬 클라이언트 소프트웨어로 다운로드한다.
Post Office Protocol(POP3): 이 프로토콜은 수신 이메일 메시지의 처리를 제어한다. 이 프로토콜을 통해 수신 이메일 메시지를 메일 서버에 보관할 수 있으며, 이메일 클라이언트 소프트웨어가 이를 접근하고 로컬 컴퓨터로 다운로드할 수 있다. 메시지가 다운로드되면 메일 서버에서 삭제된다.
Internet Mail Access Protocol(IMAP): POP3와 마찬가지로, 이 프로토콜은 수신 이메일 메시지의 처리를 제어하지만, POP3보다 더 정교하다. 수신자가 메일 서버에서 로컬 컴퓨터로 이메일 메시지를 다운로드할 때 메시지는 서버에서 삭제되지 않는다. 이 프로토콜은 수신자가 메일 클라이언트 애플리케이션, 웹 인터페이스, 스마트폰 등 여러 장치와 애플리케이션에서 메시지에 접근할 수 있도록 해준다.
Mail Transfer Agent(MTA): 전자우편을 SMTP를 이용해 다른 전자우편 서버로 전달하는 프로그램이다.
이 글에서는 spring boot 환경에서 제공하는 이메일 라이브러리와 Gmail의 SMTP 서버를 활용하여 이메일을 전송하는 기능을 구현해 볼 것이다.
spring boot의 버전은 3.2.5이며 HTML 템플릿을 사용해 이메일의 내용을 동적으로 생성하기 위해 'thymeleaf'를 사용하였다.
목차는 아래와 같다.(클릭 시 이동)
2. 의존성, application.properties(or yml) 설정
3. 메일 전송 메서드 구현 및 HTML 파일 작성( thymeleaf 사용, 변수, 이미지 삽입)
본론
1. Gmail 설정
구글 Gmail을 사용하는 것이기에 우선 구글 계정에 들어가서 설정을 해줘야 한다.
첫번째로 2단계 인증을 활성화해야 한다. 아래 사진과 같이 나오면 2단계 인증이 완료된 상태다.
이후 앱 비밀번호를 설정해야 하는데, SMTP 라이브러리를 사용할 때 비밀번호 대신 사용된다.
아래 사진과 같이 16자 비밀번호가 나오니 기억해 놓자.
2. 의존성, application.properties(or yml) 설정
우선 thymeleaf와 mail 기능을 사용하기 위해서 아래와 같이 의존성(build.gradle)을 추가해 준다.
// mail
implementation 'org.springframework.boot:spring-boot-starter-mail'
// thymeleaf
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
이후 properties(혹은 yml) 파일을 아래와 같이 작성한다.
spring.mail.host=smtp.gmail.com // 메일 서버의 호스트 이름(Gmail의 SMTP 서버)
spring.mail.port=587 // 메일 서버의 포트 번호(SMTP 서버는 일반적으로 587 포트를 사용)
spring.mail.username=khdscor@gmail.com // 메일 서버에 로그인하는 데 사용되는 사용자 이름(등록한 Gmail 주소)
spring.mail.password=[앱 비밀번호 16자] //메일 서버에 로그인하는 데 사용되는 비밀번호(등록한 앱 비밀 번호, ex.dkfgrhsdvnetghia)
spring.mail.defaultEncoding=UTF-8 // 메일 본문의 기본 인코딩
spring.mail.properties.mail.mime.charset=UTF-8 // 메일 본문의 MIME 문자셋
spring.mail.properties.mail.smtp.auth=true // SMTP 인증을 사용할지 여부
spring.mail.properties.mail.smtp.timeout=5000 // SMTP 연결 타임아웃을 밀리초 단위로 지정
spring.mail.properties.mail.smtp.starttls.enable=true // 메일 서버와의 통신에 TLS 보안을 사용할지 여부
3. 메일 전송 메서드 구현 및 HTML 파일 작성( thymeleaf 사용, 변수, 이미지 삽입)
이제 본격적으로 메일 전송을 진행하는 비즈니스 로직을 작성해 보자.
메일 전송을 위해서 자주 사용되는 것으로 JavaMailSender, SpringTemplateEngine 두 가지 있다.
JavaMailSender는 Spring Framework에서 이메일을 전송하기 위해 제공되는 인터페이스이다. 이 인터페이스는 JavaMail API를 기반으로 하며, 이메일 전송을 간편하게 처리할 수 있도록 다양한 메서드를 제공한다.
텍스트 이메일, HTML 이메일, 첨부 파일이 포함된 이메일 등을 전송할 수 있으며, 이메일을 전송하기 위해 필요한 SMTP 서버의 호스트, 포트, 인증 정보 등을 설정할 수 있다.(사전에 properties or yml 파일에 작성한 정보)
SpringTemplateEngine은 Spring Framework에서 HTML 기반의 템플릿을 처리하기 위해 주로 사용하는 Thymeleaf 템플릿 엔진의 핵심 클래스이다. SpringTemplateEngine을 사용하면 HTML 템플릿을 바탕으로 동적인 이메일 콘텐츠를 생성할 수 있다.
즉, 컨트롤러, 서비스에서 유동적인 데이터를 바탕으로 데이터를 추가한 HTML 페이지를 작성할 수 있는 것이다.
이 두 가지를 활용하여 메일 전송 메서드를 어떻게 구성할 수 있는지 확인해 보자.
우선 아래와 같이 JavaMailSender와 SpringTemplateEngine를 선언해 준다.
...
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.thymeleaf.context.Context;
import org.thymeleaf.spring6.SpringTemplateEngine;
...
@Service
@RequiredArgsConstructor
public class TestMailService {
private final JavaMailSender javaMailSender;
private final SpringTemplateEngine templateEngine;
...
}
이후 메일 전송을 위해 필요한 값들을 지정해 준다. 아래와 같이 동적으로 HTML에 추가할 변수, 이미지 파일, 메일을 전송한 주소를 지정해 주었다.
@Transactional
public String test(){
// 변수 지정
Map<String, String> emailContent = new HashMap<>();
emailContent.put("test", "하하하하");
emailContent.put("name", "호호호호");
emailContent.put("code", "히히히히");
// 서버 내 저장된 이미지 지정
List<String> images = new ArrayList<>();
images.add("static/images/main-logo.jpg");
// 메일을 전송할 이메일 주소 지정
List<String> emailList = new ArrayList<>();
emailList.add("khdscor@gmail.com");
// 메시지 전송
sendMultiEmailWithImages(emailList, emailContent, images);
return "success";
}
이제 메일을 전송하는 seneMultiEmailWithImages()를 살펴보자.
private void sendMultiEmailWithImages(
List<String> receivers,
Map<String, String> emailContent,
List<String> imagePaths) {
try {
// 이메일 전송을 위한 MimeMessageHelper 객체 생성
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
// true는 multipart 파일이 있는지 없는지를 나타내며, 이미지 파일이 있기 때문에 true로 지정
MimeMessageHelper msgHelper = new MimeMessageHelper(mimeMessage, true, "UTF-8");
// 템플릿에 매핑된 값을 설정
Context context = new Context();
emailContent.forEach(context::setVariable);
// 템플릿을 처리하여 이메일 본문 생성
String emailBody = templateEngine.process("invite-campaign", context);
// 메일 제목, 본문, 이메일 주소, 이미지 파일 지정
msgHelper.setSubject("메일 제목");
msgHelper.setText(emailBody, true);
msgHelper.setTo(receivers.toArray(new String[receivers.size()]));
// 이미지 리스트를 반복하고 각 이미지에 대해 addInline 메소드를 호출
for (int i = 0; i < imagePaths.size(); i++) {
String imagePath = imagePaths.get(i);
String contentId = "image" + (i + 1);
msgHelper.addInline(contentId, new ClassPathResource(imagePath));
}
// 이메일 전송
javaMailSender.send(msgHelper.getMimeMessage());
} catch (MessagingException e) {
throw new RuntimeException("에러 발생");
}
}
MimeMessage와 MimeMessageHelper는 복잡한 메일을 전송할 수 있도록 도와준다.
String emailBody = templateEngine.process("invite-campaign", context)는 Thymeleaf 내 SpringTemplateEngine를 활용하여 지정한 변수들과 HTML을 매핑하여 String 값으로 변환해 준다.
"invite-campaign"는 resources/templates/invite-campaign.html 파일이다.
이후 msgHelper를 통해 메일 제목, 본문, 이메일 주소, 파일을 지정해 주는데, 파일 같은 경우, 외부로 HTML파일을 전송해야 하기 때문에 msgHelper.addInline()를 통해서 지정해주어야 한다. 아래의 invite-campaign.html을 봐보자.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div style="font-family: Arial, sans-serif; margin: 0; padding: 0; background-color: #f9f9f9; max-width: 600px; margin: 0 auto; padding: 20px; border: 1px solid #dddddd;">
<div style="text-align: center; padding: 20px 0;">
<img src='cid:image1' alt="logo" style="max-width: 100%; height: auto;">
</div>
<p style="text-align: center; font-size: 18px; color: #333333; margin: 0; line-height: 1.6;">
환영
</p>
<div style="text-align: center; margin: 20px 0; padding: 20px; background-color: #ffffff; border-radius: 8px;">
<h3 style="font-size: 16px; color: #0012ce; margin: 0 0 10px 0;">세부사항:</h3>
<p style="font-size: 16px; color: #333333; margin: 0; line-height: 1.6;">
이름: <strong th:text="${name}"></strong><br>
코드: <strong th:text="${code}"></strong>
</p>
<div style = "margin-top: 20px;">
<a href="#"
style="display: inline-block; padding: 12px 24px; width: 220px; font-size: 16px; color: #ffffff; background-color: #0012ce; text-decoration: none; border-radius: 4px;">테스트 버튼</a>
</div>
</div>
</div>
</body>
</html>
<img src='cid:image1' alt="logo" style="max-width: 100%; height: auto;">에서는 자바 코드에서 지정한 contentId가 cid: 뒤에 지정한다.
이름: <strong th:text="${name}"></strong><br>에서 th: ${}를 통해 java코드에서 지정한 변수를 담은 것을 확인할 수 있다.
이후 javaMailSender.send(msgHelper.getMimeMessage());를 통해 메시지 전송을 해준다.
아래는 실행 결과이다.
아래는 java 전체 코드이다.
...
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.thymeleaf.context.Context;
import org.thymeleaf.spring6.SpringTemplateEngine;
...
@Service
@RequiredArgsConstructor
public class TestMailService {
private final JavaMailSender javaMailSender;
private final SpringTemplateEngine templateEngine;
@Transactional
public String test(){
// 변수 지정
Map<String, String> emailContent = new HashMap<>();
emailContent.put("test", "하하하하");
emailContent.put("name", "호호호호");
emailContent.put("code", "히히히히");
// 서버 내 저장된 이미지 지정
List<String> images = new ArrayList<>();
images.add("static/images/main-logo.jpg");
// 메일을 전송할 이메일 주소 지정
List<String> emailList = new ArrayList<>();
emailList.add("khdscor@gmail.com");
// 메시지 전송
sendMultiEmailWithImages(emailList, emailContent, images);
return "success";
}
private void sendMultiEmailWithImages(
List<String> receivers,
Map<String, String> emailContent,
List<String> imagePaths) {
try {
// 이메일 전송을 위한 MimeMessageHelper 객체 생성
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
// true는 multipart 파일이 있는지 없는지를 나타내며, 이미지 파일이 있기 때문에 true로 지정
MimeMessageHelper msgHelper = new MimeMessageHelper(mimeMessage, true, "UTF-8");
// 템플릿에 매핑된 값을 설정
Context context = new Context();
emailContent.forEach(context::setVariable);
// 템플릿을 처리하여 이메일 본문 생성
String emailBody = templateEngine.process("invite-campaign", context);
// 메일 제목, 본문, 이메일 주소, 이미지 파일 지정
msgHelper.setSubject("메일 제목");
msgHelper.setText(emailBody, true);
msgHelper.setTo(receivers.toArray(new String[receivers.size()]));
// 이미지 리스트를 반복하고 각 이미지에 대해 addInline 메소드를 호출
for (int i = 0; i < imagePaths.size(); i++) {
String imagePath = imagePaths.get(i);
String contentId = "image" + (i + 1);
msgHelper.addInline(contentId, new ClassPathResource(imagePath));
}
// 이메일 전송
javaMailSender.send(msgHelper.getMimeMessage());
} catch (MessagingException e) {
throw new RuntimeException("에러 발생");
}
}
}
@GetMapping("/testtest")
public String test(){
return testMailService.test();
}
결론
이렇게 Gmail SMTP 서버를 활용하여 메일 전송 기능에 대해서 알아보았다.
하지만 메일 전송 시간이 매우 길게 걸린다는 단점이 있다.
그렇기에 메일 전송을 완료하는 동안 사용자에게 응답이 지체된다는 크나큰 단점이 있다.
이를 해결하기 위해서 @Async를 통해 비동기 방식 메서드를 진행할 수 있는데, @Async를 추가한 메서드가 종료되기 전에 사용자에게 우선 응답을 할 수 있도록 하는 것이다.
이에 대한 내용은 다음 글(https://khdscor.tistory.com/132)을 참고하기 바란다.
참고
https://docs.spring.io/spring-framework/reference/integration/email.html
https://docs.cheetahces.com/en-us/messaging/product/01_Overview/Getting_Started/How_Email_Works.htm
https://velog.io/@jungmin_/%EC%9D%B4%EB%A9%94%EC%9D%BC-%EC%A0%84%EC%86%A1%ED%95%98%EA%B8%B0-SMTP
'프로젝트 관련' 카테고리의 다른 글
Spring boot - 로그백(Logback)을 통한 로그 파일 관리 (0) | 2024.10.08 |
---|---|
일일 단위로 환율 DB 저장을 위한 스케줄링 구현(with Spring boot) (0) | 2024.08.11 |
Spring boot with React: STOMP를 통해 채팅 시스템을 구현해보자(With Mysql, MongoDB)(2) (2) | 2024.05.17 |
Spring boot with React: STOMP를 통해 채팅 시스템을 구현해보자(With Mysql, MongoDB)(1) (8) | 2024.05.13 |
프로젝트 일지 - 쿼리 통합: 여러 번의 db접근을 감소시키자(with springboot, mysql, mybatis) (0) | 2023.12.25 |