06. 스프링 부트 (Spring Boot) - 자바 메일 센더 (Java Mail Sender)
OS | Windows 10 Home 64bit 버전 1903 (OS 빌드 18362.836) |
Framework | Spring Boot 2.3.3 RERELEASE |
EditTool | Inellij IDEA 2019.1.3 |
BuildTool | Gradle |
# 참고
[ Thymleaf 템플린 엔진 Guide ]
[ 메일 ]
[비동기 처리]
# 생성 및 코드
plugins {
id 'org.springframework.boot' version '2.3.3.RELEASE'
id 'mail.spring.dependency-management' version '1.0.10.RELEASE'
id 'java'
}
group = 'mail.spring'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
compile group: 'org.springframework.boot', name: 'spring-boot-starter-freemarker', version: '2.3.1.RELEASE'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
annotationProcessor 'org.projectlombok:lombok'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
testImplementation 'org.springframework.security:spring-security-test'
}
test {
useJUnitPlatform()
}
# active
spring:
profiles:
active: prod
---
# prod
spring:
profiles: prod
mail:
host: smtp.gmail.com
port: 587
username: '아이디@gmail.com'
password: '비밀번호'
properties.mail.smtp:
nickname: 'admin'
auth: true
starttls.enable: true
ssl.trust: smtp.gmail.com
# 쓰레드 풀 설정
# task.execution.pool:
# core-size: 8
# max-size: 8
---
# local ( junit5 테스트 시 )
spring:
profiles: local
mail:
host: localhost
port: 1025
properties:
mail:
stmp:
auth: false
<!-- main.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<link rel="shortcut icon" href="#">
<meta charset="UTF-8">
<title>메일 센더</title>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- b4 CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
<!-- jQuery.js / popper.js / Bootstrap4.js -->
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>
<script>
$(function () {
$('#add').click(function () {
$('.emailList').append(
' <div class="form-group">\n' +
' <div class="input-group">\n' +
' <input class="form-control" name="to" placeholder="이메일 주소">\n' +
' </div>\n' +
' </div>'
);
});
$('#async-label').click(function () {
let async = $('#async');
let bol = async.val();
async.val(bol === 'false');
});
});
</script>
</head>
<body>
<div class="container mt-5">
<h1>메일 발송</h1>
<form class="form-block" th:action="@{/mail}" method="post">
<div class="emailList">
<div class="form-group">
<div class="input-group">
<input class="form-control" name="to" placeholder="이메일 주소">
<div class="input-group-append">
<button id="add" class="btn btn-primary form-control" type="button">추가</button>
</div>
</div>
</div>
</div>
<div class="form-group">
<div class="input-group">
<input class="form-control" name="subject" placeholder="제목">
<div class="input-group-append p-2">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="async" name="async" value="false">
<label class="custom-control-label" style="user-select: none" id="async-label" for="async">비동기 처리</label>
</div>
</div>
</div>
</div>
<div class="form-group">
<select class="form-control" name="fileName">
<option value="message.html">message.html</option>
</select>
</div>
<div class="form-group">
<textarea class="form-control" name="text" placeholder="메일 내용을 입력해주세요." cols="60" rows="20"></textarea>
</div>
<div class="form-group">
<button class="form-control">발송</button>
</div>
</form>
</div>
</body>
</html>
input value 는 String 이기 때문에 !true, !false 처럼 할 수 없다
bol === 'false' 로 하여 'false' 인 경우는 true 를
true 인 경우는 false 를 입력되게하였다
<!-- message.html -->
<html xmlns:th="http://www.thymeleaf.org">
<div style="border: 3px solid red; margin-left: 20%; margin-right: 20%; margin-top: 10%">
<div style="margin: 10%; text-align: center">
<p th:text="${subject}">${title}</p>
</div>
<div style="margin: 10%; text-align: center">
<p th:text="${text}">${text}</p>
</div>
<div style="margin: 10%; text-align: center">
<p th:text="${from}">${from}</p>
</div>
</div>
</html>
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest()
.permitAll();
}
}
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;
@EnableAsync
@Configuration
public class AsyncConfig {
@Bean(name = "mailSenderExecutor")
public Executor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(3);
taskExecutor.setMaxPoolSize(30);
taskExecutor.setQueueCapacity(10);
taskExecutor.setThreadNamePrefix("Executor-");
taskExecutor.initialize();
return taskExecutor;
}
}
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MailRequestDto {
private String[] to;
private String subject;
private String text;
}
여러명에게 이메일을 보내기 위해서는 to (수신자)는 배열일 필요가 있다
import io.spring.mailsender.dto.MailRequestDto;
import io.spring.mailsender.service.MessageService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
@RequiredArgsConstructor
@Slf4j
public class MessageController {
private final MessageService messageService;
@GetMapping("/")
public String messageForm() {
info("[GET] Success messageForm()");
return "main";
}
@PostMapping("/mail")
public String messageSend(
final MailRequestDto mailRequestDto,
@RequestParam(name = "fileName", required = false) String fileName,
@RequestParam(name = "async", defaultValue = "false") boolean async ) {
info("[POST] Success messageSend()");
log.info("mailRequestDto -> {}", mailRequestDto);
log.info("fileName -> {}", fileName);
log.info("async -> {}", async);
if (!async)
messageService.mailSend(mailRequestDto, fileName);
else
messageService.mailSendWithAsync(mailRequestDto, fileName);
return "redirect:/";
}
private void info(String logMessage) {
log.info(logMessage);
}
}
boolean 은 true 는 요청이 되나, false 요청 시, 오류를 발생시킨다
org.springframework.web.bind.MissingServletRequestParameterException 발생
때문에 true 가 아닌 값은 모두 기본값으로 false 로 설정하여 바인딩한다
import io.spring.mailsender.dto.MailRequestDto;
import io.spring.mailsender.mail.MailManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.thymeleaf.context.Context;
@Service
@Slf4j
@RequiredArgsConstructor
public class MessageService {
private final MailManager mailManager;
@Value("${spring.mail.username}")
private String fromAddress;
@Value("${spring.mail.properties.mail.smtp.nickname}")
private String fromName;
@Async(value = "mailSenderExecutor")
public void mailSendWithAsync(MailRequestDto mailRequestDto, String fileName) {
send(mailRequestDto, fileName);
}
public void mailSend(MailRequestDto mailRequestDto, String fileName) {
send(mailRequestDto, fileName);
}
private void send(MailRequestDto mailRequestDto, String fileName) {
String subject = mailRequestDto.getSubject();
String text = mailRequestDto.getText();
Context context = new Context();
context.setVariable("subject", subject);
context.setVariable("text", text);
context.setVariable("from", fromAddress);
try {
mailManager.setSubject(subject);
mailManager.setThymeleafText(context, fileName, true);
mailManager.setFromName(fromAddress, fromName);
for (String to : mailRequestDto.getTo()) {
log.info("to -> {}", to);
mailManager.setTo(to);
mailManager.send();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
application.yml 에서 작성한 내용을 @Value 로 가져온다
AsyncConfig 에서 설장한 @EnableAsync 와 메소드에 @Async 를 사용하여 해당 메소드에 대해 비동기 처리를 하였고
value = "mailSenderExecutor" 는 AsyncConfig 에 있는 @Bean 을 가르킨다
@Async 는 private 메소드에 사용할 수 없다
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.mail.MailAuthenticationException;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.stereotype.Component;
import org.thymeleaf.context.Context;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
@Component
public class MailManager {
private final JavaMailSender sender;
private MimeMessage message;
private MimeMessageHelper messageHelper;
private final ThymeleafTemplateMapper thymeleafTemplateMapper;
// 생성자
public MailManager(JavaMailSender jSender, ThymeleafTemplateMapper thymeleafTemplateMapper) throws MessagingException {
this.sender = jSender;
message = jSender.createMimeMessage();
messageHelper = new MimeMessageHelper(message, true, "UTF-8");
this.thymeleafTemplateMapper = thymeleafTemplateMapper;
}
// 보내는 사람 이메일
public void setFrom(String fromAddress) throws MessagingException {
messageHelper.setFrom(fromAddress);
}
// 보내는 사람 이름
public void setFromName(String fromAddress, String fromName) throws MessagingException, UnsupportedEncodingException {
messageHelper.setFrom(fromAddress, fromName);
}
// 받는 사람 이메일
public void setTo(String email) throws MessagingException {
messageHelper.setTo(email);
}
// 제목
public void setSubject(String subject) throws MessagingException {
messageHelper.setSubject(subject);
}
// 메일 내용
public void setText(String text, boolean useHtml) throws MessagingException {
messageHelper.setText(text, useHtml);
}
// 메일 내용
public void setThymeleafText(Context context, String fileName, boolean useHtml) throws MessagingException {
String resultText = thymeleafTemplateMapper.parse(context, fileName);
System.out.println(resultText);
messageHelper.setText(resultText, useHtml);
}
// 첨부 파일
public void setAttach(String displayFileName, String pathToAttachment) throws MessagingException, IOException {
File file = new ClassPathResource(pathToAttachment).getFile();
FileSystemResource fsr = new FileSystemResource(file);
messageHelper.addAttachment(displayFileName, fsr);
}
// 이미지 삽입
public void setInline(String contentId, String pathToInline) throws MessagingException, IOException {
File file = new ClassPathResource(pathToInline).getFile();
FileSystemResource fsr = new FileSystemResource(file);
messageHelper.addInline(contentId, fsr);
}
// 발송
public void send() {
try {
sender.send(message);
} catch (MailAuthenticationException e) {
e.printStackTrace();
throw new IllegalArgumentException("계정 인증 실패");
}catch(Exception e) {
e.printStackTrace();
}
}
}
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
@Component
@RequiredArgsConstructor
public class ThymeleafTemplateMapper {
private final TemplateEngine templateEngine;
public String parse(Context context, String fileName) {
return templateEngine.process(fileName, context);
}
}
thymeleaf 템플릿 엔진과 thymeleaf 콘텍스트를 이용하여 미리 작성한 thymeleaf 문서를 binding 하여 string 하여 반환 시킬 수 있다.
[ Too Much Infomation ]
TemplateEngine 은 Gradle 에서 의존성을 추가했기때문에 스프링에서 빈으로 생성하여 자동주입한다
TemplateEngine 에서 preffix 와 suffix 를 설정할 수 있다
import io.spring.mailsender.dto.MailRequestDto;
import io.spring.mailsender.mail.MailManager;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.web.servlet.ViewResolver;
import org.thymeleaf.context.Context;
import org.thymeleaf.spring5.SpringTemplateEngine;
import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;
import org.thymeleaf.spring5.view.ThymeleafViewResolver;
import javax.mail.MessagingException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@ExtendWith({SpringExtension.class, MockitoExtension.class})
class MessageServiceTests {
@TestConfiguration
static class ThymeleafTemplateMapper {
@Bean
public SpringResourceTemplateResolver templateResolver() {
SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver ();
templateResolver.setPrefix("classpath:templates/");
templateResolver.setCharacterEncoding("UTF-8");
templateResolver.setSuffix(".html");
templateResolver.setTemplateMode("LEGACYHTML5");
templateResolver.setCacheable(true);
return templateResolver;
}
@Bean
public SpringTemplateEngine templateEngine(MessageSource messageSource) {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver());
templateEngine.setTemplateEngineMessageSource(messageSource);
return templateEngine;
}
@Bean
@Autowired
public ViewResolver viewResolver(MessageSource messageSource) {
ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
viewResolver.setTemplateEngine(templateEngine(messageSource));
viewResolver.setCharacterEncoding("UTF-8");
viewResolver.setOrder(0);
return viewResolver;
}
}
@Autowired
private SpringTemplateEngine template;
@Mock
private MailManager mockMailManager;
private MessageService messageService;
@BeforeEach
void setUp() {
messageService = new MessageService(mockMailManager);
}
@Test
void mailSend() throws MessagingException {
// request
String[] to = {"test@naver.com", "test@gmail.com"};
String subject = "제목";
String text = "내용";
String fileName = "message.html";
// @Value
String fromAddress = "admin@gmail.com";
// dto
MailRequestDto mailRequestDto = MailRequestDto.builder()
.to(to)
.subject(subject)
.text(text)
.build();
messageService.mailSend(mailRequestDto, fileName);
// setThymeleafText() 메소드 확인
// [선택 1]
verify(mockMailManager).setThymeleafText(any(), eq("message.html"), eq(true));
// [선택 2]
// verify(mockMailManager).setThymeleafText(any(), anyString(), anyBoolean());
// send() 메소드 확인 (2번 실행 됐는 지)
verify(mockMailManager, times(2)).send();
Context context = new Context();
context.setVariable("subject", subject);
context.setVariable("text", text);
context.setVariable("from", fromAddress);
String result = template.process(fileName, context);
System.out.println(result);
}
}
JUnit 5 Mockito 테스트 코드
메일 전송 처리속도는 비슷비슷 합니다
동기처리는 사용자가 서버가 처리하는 것을 기다려야 하는 반면,
비동기처리는 사용자가 서버가 처리하는 것을 기다리지 않습니다.
비동기 처리는 대신 쓰레드를 생성이 하기 때문에, 많은 경우 서버에 부담을 줍니다
[+참고]
JavaMailSender, MimeMessage, MimeMessageHelper 클래스들은 외부 라이브러리 입니다
[+참고]
메일에서는 <script></script> 구문은 실행되지 않습니다
<link href="CDN"> 를 이용한 CSS 도 적용되지 않습니다
<div style="border: 3px solid blue"></div> 처럼 style 속성을 사용해야 CSS 가 적용된다
[+참고]
구글은 짧은시간이 기준인지 같은 문장이 기준인지 몰라도 2번째 보낼 때 조건에 걸리면 안보내집니다
아마 매크로 같은 거나 서버에 부담을 주는 요소에 대해서는 처리를 하지 않는 것 같아요
네이버는 그런거 없이 메일 전송이 잘 됩니다
'10. Spring > BOOT' 카테고리의 다른 글
스프링 부트 MVC - Handler Interceptor (0) | 2022.04.14 |
---|---|
11. 스프링 부트 (Spring Boot 2) & 뷰 (Vue 2)- 환경 구축하기 (0) | 2021.03.26 |
10. 스프링부트 (Spring Boot 2.4.3) - javax Transactional과 spring Transactional (0) | 2021.03.14 |
09. 스프링 부트 (Spring Boot) - thymeleaf 페이징 처리 Pageable [미완성] (0) | 2020.11.19 |
02. 스프링부트 (Spring Boot) Profile, JPA Naming, Exception 전략 (0) | 2020.07.11 |
댓글
이 글 공유하기
다른 글
-
11. 스프링 부트 (Spring Boot 2) & 뷰 (Vue 2)- 환경 구축하기
11. 스프링 부트 (Spring Boot 2) & 뷰 (Vue 2)- 환경 구축하기
2021.03.26 -
10. 스프링부트 (Spring Boot 2.4.3) - javax Transactional과 spring Transactional
10. 스프링부트 (Spring Boot 2.4.3) - javax Transactional과 spring Transactional
2021.03.14 -
09. 스프링 부트 (Spring Boot) - thymeleaf 페이징 처리 Pageable [미완성]
09. 스프링 부트 (Spring Boot) - thymeleaf 페이징 처리 Pageable [미완성]
2020.11.19 -
02. 스프링부트 (Spring Boot) Profile, JPA Naming, Exception 전략
02. 스프링부트 (Spring Boot) Profile, JPA Naming, Exception 전략
2020.07.11