Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 회원가입 이메일 전송 비동기 구현 및 가지 않은 메일 처리해주는 스케줄러 생성 #9

Merged
merged 10 commits into from
Jan 14, 2024
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ dependencies {
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"

// mail-sender
implementation 'org.springframework.boot:spring-boot-starter-mail'
}

def generated = 'src/main/generated'
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/market/ElectronicMarketApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.scheduling.annotation.EnableAsync;

import java.util.TimeZone;

@EnableJpaAuditing
@EnableAsync
@SpringBootApplication
public class ElectronicMarketApplication {

Expand Down
25 changes: 25 additions & 0 deletions src/main/java/com/market/alarm/application/MailEventHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.market.alarm.application;

import com.market.alarm.domain.MailStorage;
import com.market.member.domain.auth.RegisteredEvent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;

@Slf4j
@RequiredArgsConstructor
@Service
public class MailEventHandler {

private final MailService mailService;

@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void sendMail(final RegisteredEvent event) {
MailStorage mailStorage = MailStorage.createDefault(event.getMemberId(), event.getEmail(), event.getNickname());
mailService.sendMail(mailStorage);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.market.alarm.application;

import lombok.RequiredArgsConstructor;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

@RequiredArgsConstructor
@Service
public class MailScheduleService {

private final MailService mailService;

@Scheduled(cron = "0 */10 * * * *")
public void resendMail() {
mailService.resendMail();
}

@Scheduled(cron = "0 */15 * * * *")
public void deleteSendSuccessMails() {
mailService.deleteSuccessMails();
}
}
74 changes: 74 additions & 0 deletions src/main/java/com/market/alarm/application/MailService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.market.alarm.application;

import com.market.alarm.domain.MailSender;
import com.market.alarm.domain.MailStorage;
import com.market.alarm.domain.MailStorageRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicBoolean;

@Slf4j
@RequiredArgsConstructor
@Service
public class MailService {

private static final int THREAD_COUNT = 4;

private final MailStorageRepository mailStorageRepository;
private final MailSender mailSender;
private final AtomicBoolean isRunning = new AtomicBoolean(false);

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void sendMail(final MailStorage mailStorage) {
send(mailStorage);
}

private void send(final MailStorage mailStorage) {
log.info("{} 번 유저 생성. 닉네임 : {}, 메일 발송 시도!", mailStorage.getReceiverId(), mailStorage.getReceiverNickname());

try {
mailSender.pushMail(mailStorage.getReceiverEmail(), mailStorage.getId(), mailStorage.getReceiverNickname());
mailStorage.updateStatusDone();
log.info("{} 번 유저 생성. 닉네임 : {}, 메일 발송 성공!", mailStorage.getReceiverId(), mailStorage.getReceiverNickname());
} catch (final Exception exception) {
handleErrors(mailStorage, exception);
}
}

private void handleErrors(final MailStorage mailStorage, final Exception exception) {
log.info("{} 번 유저 생성. 닉네임 : {}, 메일 발송 실패!", mailStorage.getReceiverId(), mailStorage.getReceiverNickname());
log.error(exception.getMessage());

mailStorage.updateStatusFail();
mailStorageRepository.save(mailStorage);
}

@Transactional
public void resendMail() {
List<MailStorage> sendFailureMails = mailStorageRepository.findAllByNotDone();

if (isRunning.compareAndSet(false, true)) {
ExecutorService executorService = Executors.newFixedThreadPool(THREAD_COUNT);

for (MailStorage sendFailureMail : sendFailureMails) {
executorService.submit(() -> send(sendFailureMail));
}

executorService.shutdown();
isRunning.set(false);
log.info("실패 메일 재전송 성공! 건수: {}", sendFailureMails.size());
}
}

@Transactional
public void deleteSuccessMails() {
mailStorageRepository.deleteAllByDoneMails();
}
}
35 changes: 35 additions & 0 deletions src/main/java/com/market/alarm/config/MailSource.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.market.alarm.config;

import lombok.AllArgsConstructor;
import lombok.Getter;

@AllArgsConstructor
@Getter
public enum MailSource {

TITLE("E-market 회원가입 성공 안내"),
CONTENT("""
<h1> E-market </h1>
<br>
<p>E-market 회원가입에 성공하셨습니다.<p>
<br>
<p>해당 이메일은 회원가입 성공 안내 메시지입니다.<p>
<br>
<p>회원가입 기념 쿠폰을 발급하였습니다.<p>
<br>
""");

private final String message;

public static String getMailMessage(final Long id, final String nickname) {
String message = "";

message += TITLE.message;
message += "<div style='margin:100px;'>";
message += "<p>" + id + "번 유저인" + nickname + "님 환영합니다.<p>";
message += CONTENT.message;
message += "</div>";

return message;
}
}
21 changes: 21 additions & 0 deletions src/main/java/com/market/alarm/config/ResendScheduleRunner.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.market.alarm.config;

import com.market.alarm.application.MailScheduleService;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Configuration;

@RequiredArgsConstructor
@ConditionalOnProperty(name = "schedule.mail", havingValue = "true")
@Configuration
public class ResendScheduleRunner implements ApplicationRunner {

private final MailScheduleService mailScheduleService;

@Override
public void run(final ApplicationArguments args) {
mailScheduleService.resendMail();
}
}
12 changes: 12 additions & 0 deletions src/main/java/com/market/alarm/domain/MailSender.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.market.alarm.domain;

import jakarta.mail.MessagingException;

import java.io.UnsupportedEncodingException;

public interface MailSender {

void pushMail(final String receiver,
final Long id,
final String nickname) throws MessagingException, UnsupportedEncodingException;
}
8 changes: 8 additions & 0 deletions src/main/java/com/market/alarm/domain/MailStatus.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.market.alarm.domain;

public enum MailStatus {

WAIT,
FAIL,
DONE
}
70 changes: 70 additions & 0 deletions src/main/java/com/market/alarm/domain/MailStorage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.market.alarm.domain;

import com.market.global.domain.BaseEntity;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Builder
@EqualsAndHashCode(of = "id", callSuper = false)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class MailStorage extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Embedded
private Receiver receiver;

@Enumerated(value = EnumType.STRING)
private MailStatus mailStatus;

public static MailStorage createDefault(final Long memberId, final String email, final String nickname) {
return getMailStorage(memberId, email, nickname, MailStatus.WAIT);
}

public static MailStorage createByStatus(final Long memberId, final String email, final String nickname, final MailStatus mailStatus) {
return getMailStorage(memberId, email, nickname, mailStatus);
}

private static MailStorage getMailStorage(final Long memberId, final String email, final String nickname, final MailStatus mailStatus) {
return MailStorage.builder()
.receiver(Receiver.createDefault(memberId, email, nickname))
.mailStatus(mailStatus)
.build();
}

public void updateStatusFail() {
this.mailStatus = MailStatus.FAIL;
}

public void updateStatusDone() {
this.mailStatus = MailStatus.DONE;
}

public Long getReceiverId() {
return receiver.getMemberId();
}

public String getReceiverEmail() {
return receiver.getEmail();
}

public String getReceiverNickname() {
return receiver.getNickname();
}
}
12 changes: 12 additions & 0 deletions src/main/java/com/market/alarm/domain/MailStorageRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.market.alarm.domain;

import java.util.List;

public interface MailStorageRepository {

void save(final MailStorage mailStorage);

List<MailStorage> findAllByNotDone();

void deleteAllByDoneMails();
}
28 changes: 28 additions & 0 deletions src/main/java/com/market/alarm/domain/Receiver.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.market.alarm.domain;

import jakarta.persistence.Embeddable;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Embeddable
public class Receiver {

private Long memberId;
private String email;
private String nickname;

protected static Receiver createDefault(final Long memberId, final String email, final String nickname) {
return Receiver.builder()
.memberId(memberId)
.email(email)
.nickname(nickname)
.build();
}
}
55 changes: 55 additions & 0 deletions src/main/java/com/market/alarm/infrastructure/EmailSender.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.market.alarm.infrastructure;

import com.market.alarm.config.MailSource;
import com.market.alarm.domain.MailSender;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeMessage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.stereotype.Component;

import java.io.UnsupportedEncodingException;

import static jakarta.mail.Message.RecipientType.TO;

@Slf4j
@RequiredArgsConstructor
@Component
public class EmailSender implements MailSender {

private static final String CHARSET = "utf-8";
private static final String SUBTYPE = "html";

@Value("${mail.sender.email}")
private String email;

@Value("${mail.sender.name}")
private String name;

private final JavaMailSender javaMailSender;

@Override
public void pushMail(final String receiver,
final Long id,
final String nickname) throws MessagingException, UnsupportedEncodingException {
MimeMessage message = createMessage(receiver, id, nickname);
javaMailSender.send(message);
}


private MimeMessage createMessage(final String receiver,
final Long id,
final String nickname) throws MessagingException, UnsupportedEncodingException {
MimeMessage message = javaMailSender.createMimeMessage();

message.addRecipients(TO, receiver);
message.setSubject(MailSource.TITLE.getMessage());
message.setText(MailSource.getMailMessage(id, nickname), CHARSET, SUBTYPE);
message.setFrom(new InternetAddress(email, name));

return message;
}
}