Skip to content

Commit

Permalink
메일 배치 전송 사이즈 변경 (#41)
Browse files Browse the repository at this point in the history
* refactor: 필드 주입 대신 생성자 주입으로 변경

* refactor: 개행 및 EOL 추가

* refactor: 서류, 최종 메일 분리 및 비동기 처리

* refactor: 빌더 생성자로 변경

* test: displayName 추가

* fix: 스레드 작업 대기 시간 연장

* feat: 메일 예외 추가

* feat: 메일 병렬처리 추가

* fix: 대기시간 연장

* refactor: 메일 예약, 메일 전송 로직 분리

* fix: 메일 전송 테스트 비활성화

* fix: setter 추가

* feat: 서류 합격 메일 전송

* fix: 템플릿 정구 수

* feat: 메일 전송 비동기, 40개 배치 전송

* feat: 메일 타임아웃 설정

* fix: 비동기 스레드풀 max 변경

* fix: 메일 패키지 변경

* fix: Mail record에서 class로 변경

* fix: batch size 변경

* fix: 메일 수신자 타입 변경
  • Loading branch information
ay-eonii authored Nov 12, 2024
1 parent b29837c commit 6c88e75
Show file tree
Hide file tree
Showing 13 changed files with 81 additions and 64 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import land.leets.global.cron.SendMailCron;
import land.leets.domain.mail.usecase.SendMailCron;
import land.leets.global.error.ErrorResponse;
import lombok.RequiredArgsConstructor;

Expand Down
13 changes: 13 additions & 0 deletions src/main/java/land/leets/domain/mail/domain/Mail.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package land.leets.domain.mail.domain;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class Mail {

private final String title;
private final String to;
private final String body;
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package land.leets.global.mail.exception;
package land.leets.domain.mail.exception;

import land.leets.global.error.ErrorCode;
import land.leets.global.error.exception.ServiceException;

public class MailException extends ServiceException {

public MailException() {
super(ErrorCode.MAIL_SEND_FAIL);
}
public MailException() {
super(ErrorCode.MAIL_SEND_FAIL);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package land.leets.global.mail;
package land.leets.domain.mail.service;

import java.util.List;
import java.util.Objects;
Expand All @@ -13,8 +13,8 @@
import jakarta.mail.Address;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import land.leets.global.mail.dto.MailDto;
import land.leets.global.mail.exception.MailException;
import land.leets.domain.mail.domain.Mail;
import land.leets.domain.mail.exception.MailException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

Expand All @@ -25,13 +25,13 @@ public class MailFactory {

private final JavaMailSender javaMailSender;

public MimeMessage createMail(MailDto mailDto) {
public MimeMessage createMail(Mail mail) {
try {
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, false, "UTF-8");
mimeMessageHelper.setTo(mailDto.to());
mimeMessageHelper.setSubject(mailDto.title());
mimeMessageHelper.setText(mailDto.body(), true);
mimeMessageHelper.setTo(mail.getTo());
mimeMessageHelper.setSubject(mail.getTitle());
mimeMessageHelper.setText(mail.getBody(), true);
return mimeMessage;
} catch (MessagingException e) {
throw new MailException();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
package land.leets.global.mail;
package land.leets.domain.mail.service;

import java.util.List;
import java.util.stream.IntStream;

import org.springframework.stereotype.Component;

import jakarta.mail.internet.MimeMessage;
import land.leets.global.mail.dto.MailDto;
import land.leets.domain.mail.domain.Mail;
import lombok.RequiredArgsConstructor;

@Component
@RequiredArgsConstructor
public class MailManager {

private static final int BATCH_SIZE = 40;
private static final int BATCH_SIZE = 30;

private final MailFactory mailFactory;

public void sendEmails(List<MailDto> mailDtos) {
List<List<MimeMessage>> batchedMails = createChunkedMails(mailDtos);
public void sendEmails(List<Mail> mail) {
List<List<MimeMessage>> batchedMails = createChunkedMails(mail);

batchedMails.parallelStream()
.forEach(mailFactory::sendMails);
}

private List<List<MimeMessage>> createChunkedMails(List<MailDto> mailDtos) {
List<MimeMessage> messages = mailDtos.parallelStream()
private List<List<MimeMessage>> createChunkedMails(List<Mail> mail) {
List<MimeMessage> messages = mail.parallelStream()
.map(mailFactory::createMail)
.toList();
return partitionMessages(messages);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
import land.leets.domain.application.domain.Application;
import land.leets.domain.application.domain.repository.ApplicationRepository;
import land.leets.domain.application.type.ApplicationStatus;
import land.leets.global.mail.MailManager;
import land.leets.global.mail.dto.MailDto;
import land.leets.domain.mail.domain.Mail;
import land.leets.domain.mail.service.MailManager;
import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
Expand All @@ -23,6 +23,9 @@
public class SendFinalMailImpl implements SendMail {

private static final String MAIL_TITLE = "[Leets] 면접 결과 안내 메일입니다.";
private static final String NAME_FIELD = "name";
private static final String THEME_FIELD = "theme";
private static final int THEME_COUNT = 3;
private static final Map<ApplicationStatus, String> templates = Map.of(
ApplicationStatus.PASS, "Pass.html",
ApplicationStatus.FAIL, "Fail.html"
Expand All @@ -37,21 +40,21 @@ public class SendFinalMailImpl implements SendMail {
public void execute(ApplicationStatus status) {
List<Application> applications = applicationRepository.findAllByApplicationStatus(status);

List<MailDto> mailDtos = new ArrayList<>();
List<Mail> mails = new ArrayList<>();
for (Application application : applications) {
Context context = makeContext(application.getName());
String message = templateEngine.process(templates.get(status), context);
MailDto mailDto = new MailDto(MAIL_TITLE, new String[] {application.getUser().getEmail()}, message);
mailDtos.add(mailDto);
Mail mail = new Mail(MAIL_TITLE, application.getUser().getEmail(), message);
mails.add(mail);
}
mailManager.sendEmails(mailDtos);
mailManager.sendEmails(mails);
}

private Context makeContext(String name) {
Context context = new Context();
context.setVariable("name", name);
int themeNumber = RANDOM.nextInt(3) + 1;
context.setVariable("theme", themeNumber);
context.setVariable(NAME_FIELD, name);
int themeNumber = RANDOM.nextInt(THEME_COUNT) + 1;
context.setVariable(THEME_FIELD, themeNumber);
return context;
}
}
3 changes: 2 additions & 1 deletion src/main/java/land/leets/domain/mail/usecase/SendMail.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
import land.leets.domain.application.type.ApplicationStatus;

public interface SendMail {
void execute(ApplicationStatus status);

void execute(ApplicationStatus status);
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package land.leets.global.cron;
package land.leets.domain.mail.usecase;

import org.springframework.stereotype.Component;

import land.leets.domain.application.type.ApplicationStatus;
import land.leets.domain.mail.usecase.SendFinalMailImpl;
import land.leets.domain.mail.usecase.SendPaperMailImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

Expand Down
48 changes: 28 additions & 20 deletions src/main/java/land/leets/domain/mail/usecase/SendPaperMailImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,26 @@
import land.leets.domain.interview.domain.repository.InterviewRepository;
import land.leets.domain.interview.exception.InterviewNotFoundException;
import land.leets.domain.interview.type.HasInterview;
import land.leets.global.mail.MailManager;
import land.leets.global.mail.dto.MailDto;
import land.leets.domain.mail.domain.Mail;
import land.leets.domain.mail.service.MailManager;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class SendPaperMailImpl implements SendMail {

private static final String MAIL_TITLE = "[Leets] 서류 결과 안내 메일입니다.";
private static final String NAME_FIELD = "name";
private static final String THEME_FIELD = "theme";
private static final int THEME_COUNT = 3;
private static final String ENV_PROD = "prod";
private static final String UID_FIELD = "uid";
private static final String HAS_INTERVIEW_FIELD = "hasInterview";
private static final String ATTEND_URL_FIELD = "attendUrl";
private static final String ABSENT_URL_FIELD = "absentUrl";
private static final String FIXED_INTERVIEW_DATE_FIELD = "fixedInterviewDate";
private static final String INTERVIEW_PLACE_FIELD = "interviewPlace";
private static final Map<ApplicationStatus, String> templates = Map.of(
ApplicationStatus.PASS_PAPER, "PassPaper.html",
ApplicationStatus.FAIL_PAPER, "FailPaper.html"
Expand All @@ -60,43 +68,43 @@ public class SendPaperMailImpl implements SendMail {
public void execute(ApplicationStatus status) {
List<Application> applications = applicationRepository.findAllByApplicationStatus(status);

List<MailDto> mailDtos = new ArrayList<>();
List<Mail> mails = new ArrayList<>();
for (Application application : applications) {
Context context = makeContext(application.getName());
if (status == ApplicationStatus.PASS_PAPER) {
setInterviewContext(context, application);
}
String message = templateEngine.process(templates.get(status), context);
MailDto mailDto = new MailDto(MAIL_TITLE, new String[] {application.getUser().getEmail()}, message);
mailDtos.add(mailDto);
Mail mail = new Mail(MAIL_TITLE, application.getUser().getEmail(), message);
mails.add(mail);
}
mailManager.sendEmails(mailDtos);
mailManager.sendEmails(mails);
}

private Context makeContext(String name) {
Context context = new Context();
context.setVariable("name", name);
int themeNumber = RANDOM.nextInt(3) + 1;
context.setVariable("theme", themeNumber);
context.setVariable(NAME_FIELD, name);
int themeNumber = RANDOM.nextInt(THEME_COUNT) + 1;
context.setVariable(THEME_FIELD, themeNumber);
return context;
}

private void setInterviewContext(Context context, Application application) {
boolean isProd = Arrays.stream(environment.getActiveProfiles())
.anyMatch(env -> env.equalsIgnoreCase("prod"));
.anyMatch(env -> env.equalsIgnoreCase(ENV_PROD));

UUID uid = application.getUser().getUid();
UriComponents attendUrl = UriComponentsBuilder.fromHttpUrl(isProd ? SERVER_TARGET_URL : LOCAL_TARGET_URL)
.queryParam("uid", uid)
.queryParam("hasInterview", HasInterview.CHECK)
.queryParam(UID_FIELD, uid)
.queryParam(HAS_INTERVIEW_FIELD, HasInterview.CHECK)
.build();
context.setVariable("attendUrl", attendUrl);
context.setVariable(ATTEND_URL_FIELD, attendUrl);

UriComponents absentUrl = UriComponentsBuilder.fromHttpUrl(isProd ? SERVER_TARGET_URL : LOCAL_TARGET_URL)
.queryParam("uid", uid)
.queryParam("hasInterview", HasInterview.UNCHECK)
.queryParam(UID_FIELD, uid)
.queryParam(HAS_INTERVIEW_FIELD, HasInterview.UNCHECK)
.build();
context.setVariable("absentUrl", absentUrl);
context.setVariable(ABSENT_URL_FIELD, absentUrl);

Interview interview = interviewRepository.findByApplication(application)
.orElseThrow(InterviewNotFoundException::new);
Expand All @@ -105,7 +113,7 @@ private void setInterviewContext(Context context, Application application) {
.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG).withLocale(Locale.KOREAN));
String time = interview.getFixedInterviewDate()
.format(DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).withLocale(Locale.KOREAN));
context.setVariable("fixedInterviewDate", date + " " + time);
context.setVariable("interviewPlace", interview.getPlace());
context.setVariable(FIXED_INTERVIEW_DATE_FIELD, date + " " + time);
context.setVariable(INTERVIEW_PLACE_FIELD, interview.getPlace());
}
}
2 changes: 1 addition & 1 deletion src/main/java/land/leets/global/config/AsyncConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public class AsyncConfig {
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(4);
executor.setMaxPoolSize(2);
executor.setQueueCapacity(200);
executor.setThreadNamePrefix("Mailing-");
executor.setKeepAliveSeconds(60);
Expand Down
8 changes: 0 additions & 8 deletions src/main/java/land/leets/global/mail/dto/MailDto.java

This file was deleted.

4 changes: 3 additions & 1 deletion src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ spring:
mail:
smtp:
auth: true
timeout: 5000
timeout: 10000 # 응답 타임아웃 (10초)
connectiontimeout: 5000 # 연결 타임아웃 (5초)
writetimeout: 5000 # 쓰기 타임아웃 (5초)
starttls:
enable: true
thymeleaf:
Expand Down
4 changes: 2 additions & 2 deletions src/test/java/land/leets/TestFixture.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
import land.leets.domain.application.type.ApplicationStatus;
import land.leets.domain.application.type.Position;
import land.leets.domain.application.type.SubmitStatus;
import land.leets.domain.mail.domain.Mail;
import land.leets.domain.user.domain.User;
import land.leets.global.mail.dto.MailDto;

public class TestFixture {

Expand All @@ -19,5 +19,5 @@ public class TestFixture {
"schedule", "capability", "conflict", "passion", LocalDateTime.now(),
ApplicationStatus.PASS, SubmitStatus.SUBMIT);

public static MailDto mailDto = new MailDto("제목", new String[] {"[email protected]"}, "body");
public static Mail mail = new Mail("제목", new String[] {"[email protected]"}, "body");
}

0 comments on commit 6c88e75

Please sign in to comment.