비즈니스 요구사항이 빈번히 변경되는 지점에 추상화를 적용하여 클라이언트 코드 변경 전파 최소화
요약
유사한 성격의 메일을 발송하는 두 클래스를 추상 클래스를 선언하고 템플릿 메서드 패턴을 사용하여 공통 로직을 한 곳으로 모아서 중복을 제거했습니다.
구독자에게 메일을 보내는 종류는 크게 두 가지가 있고 객체가 분리되어 있습니다. 첫 번째는 공지, 인증, 관리자 보고 등의 메일을 발송하는 MailSender
클래스가 있고, 두 번째는 질문지를 발송하는 QuestionSender
입니다.
@Slf4j
@Component(value = "emailSender")
@RequiredArgsConstructor
public class MailSender {
private static final int MAIL_SENDER_RATE_MILLISECONDS = 500;
private static final String FROM_EMAIL = "maeil-mail <maeil-mail-noreply@maeil-mail.site>";
private final JavaMailSender javaMailSender;
private final MailEventRepository mailEventRepository;
@Async
public void sendMail(MailMessage message) {
try {
log.info("메일을 전송합니다. email = {} question = {} type = {}", message.to(), message.subject(), message.type());
MimeMessage mimeMessage = convertToMime(message);
javaMailSender.send(mimeMessage);
mailEventRepository.save(MailEvent.success(message.to(), message.type()));
} catch (MessagingException | MailException e) {
log.error("메일 전송 실패: email = {}, type = {}, 오류 = {}", message.to(), message.type(), e.getMessage(), e);
mailEventRepository.save(MailEvent.fail(message.to(), message.type()));
} catch (Exception e) {
log.error("예기치 않은 오류 발생: email = {}, type = {}, 오류 = {}", message.to(), message.type(), e.getMessage(), e);
mailEventRepository.save(MailEvent.fail(message.to(), message.type()));
} finally {
try {
Thread.sleep(MAIL_SENDER_RATE_MILLISECONDS);
} catch (InterruptedException ignored) {
}
}
}
private MimeMessage convertToMime(MailMessage message) throws MessagingException {
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
tryAppendOpenEventTrace(message, mimeMessage);
MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, false, "UTF-8");
mimeMessageHelper.setFrom(FROM_EMAIL);
mimeMessageHelper.setTo(message.to());
mimeMessageHelper.setSubject("[매일메일] " + message.subject());
mimeMessageHelper.setText(message.text(), true);
return mimeMessage;
}
private void tryAppendOpenEventTrace(MailMessage message, MimeMessage mimeMessage) throws MessagingException {
if (message.type().startsWith("question")) {
mimeMessage.setHeader("X-SES-CONFIGURATION-SET", "my-first-configuration-set");
mimeMessage.setHeader("X-SES-MESSAGE_TAGS", "mail-open");
}
}
}
public record MailMessage(String to, String subject, String text, String type) {
}
@Slf4j
@Component(value = "questionSender")
@RequiredArgsConstructor
public class QuestionSender {
private static final int MAIL_SENDER_RATE_MILLISECONDS = 500;
private static final String FROM_EMAIL = "maeil-mail <maeil-mail-noreply@maeil-mail.site>";
private final JavaMailSender javaMailSender;
private final SubscribeQuestionRepository subscribeQuestionRepository;
@Async
public void sendMail(SubscribeQuestionMessage message) {
Subscribe subscribe = message.subscribe();
Question question = message.question();
QuestionCategory questionCategory = question.getCategory();
String to = subscribe.getEmail();
String subject = "[매일메일] " + message.subject();
String text = message.text();
String category = questionCategory.toLowerCase();
try {
log.info("질문지를 전송합니다. email = {}, questionId = {}, subject = {}, category = {}", to, question.getId(), subject, category);
MimeMessage mimeMessage = convertToMime(to, subject, text);
javaMailSender.send(mimeMessage);
subscribeQuestionRepository.save(SubscribeQuestion.success(subscribe, question));
} catch (MessagingException | MailException e) {
log.error("메일 전송 실패: email = {}, questionId = {}, subject = {}, category = {}, 오류 = {}", to, question.getId(), subject, category.toLowerCase(), e.getMessage(), e);
subscribeQuestionRepository.save(SubscribeQuestion.fail(subscribe, question));
} catch (Exception e) {
log.error("예기치 않은 오류 발생: email = {}, questionId = {}, subject = {}, category = {}, 오류 = {}", to, question.getId(), subject, category.toLowerCase(), e.getMessage(), e);
subscribeQuestionRepository.save(SubscribeQuestion.fail(subscribe, question));
} finally {
try {
Thread.sleep(MAIL_SENDER_RATE_MILLISECONDS);
} catch (InterruptedException ignored) {
}
}
}
private MimeMessage convertToMime(String to, String subject, String text) throws MessagingException {
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
tryAppendOpenEventTrace(mimeMessage);
MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, false, "UTF-8");
mimeMessageHelper.setFrom(FROM_EMAIL);
mimeMessageHelper.setTo(to);
mimeMessageHelper.setSubject(subject);
mimeMessageHelper.setText(text, true);
return mimeMessage;
}
private void tryAppendOpenEventTrace(MimeMessage mimeMessage) throws MessagingException {
mimeMessage.setHeader("X-SES-CONFIGURATION-SET", "my-first-configuration-set");
mimeMessage.setHeader("X-SES-MESSAGE_TAGS", "mail-open");
}
}
public record SubscribeQuestionMessage(
Subscribe subscribe,
Question question,
String subject,
String text
) {
}
두 클래스는 메일을 발송할 때 로직이 거의 유사하여 코드의 중복이 많은 상태였습니다. 주요한 차이점으로는 MailSender는 MailEventRepository에 MailEvent를 저장하고, QuestionSender는 SubscribeQuestionRepository에 SubscribeQuestion을 저장한다는 것입니다.
이를 해결하고자 추상 클래스를 선언하여 sendMail 메서드의 흐름을 템플릿 메서드로 정의하고, 구체적인 부분만 상속받은 클래스에서 구현하도록 리팩터링 했습니다.
@Slf4j
@RequiredArgsConstructor
public abstract class AbstractMailSender<T> {
private static final int MAIL_SENDER_RATE_MILLISECONDS = 500;
protected static final String FROM_EMAIL = "maeil-mail <maeil-mail-noreply@maeil-mail.site>";
protected final JavaMailSender javaMailSender;
@Async
public void sendMail(T message) {
try {
logSending(message);
MimeMessage mimeMessage = createMimeMessage(message);
javaMailSender.send(mimeMessage);
handleSuccess(message);
} catch (MessagingException | MailException e) {
log.error("메일 전송 실패: {}", e.getMessage(), e);
handleFailure(message);
} catch (Exception e) {
log.error("예기치 않은 오류 발생: {}", e.getMessage(), e);
handleFailure(message);
} finally {
try {
Thread.sleep(MAIL_SENDER_RATE_MILLISECONDS);
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
}
}
}
protected abstract void logSending(T message);
protected abstract MimeMessage createMimeMessage(T message) throws MessagingException;
protected abstract void handleSuccess(T message);
protected abstract void handleFailure(T message);
}
@Slf4j
@Component("emailSender")
public class MailSender extends AbstractMailSender<MailMessage> {
private final MailEventRepository mailEventRepository;
public MailSender(JavaMailSender javaMailSender, MailEventRepository mailEventRepository) {
super(javaMailSender);
this.mailEventRepository = mailEventRepository;
}
@Override
protected void logSending(MailMessage message) {
log.info("메일을 전송합니다. email = {} subject = {} type = {}", message.to(), message.subject(), message.type());
}
@Override
protected MimeMessage createMimeMessage(MailMessage message) throws MessagingException {
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, false, "UTF-8");
helper.setFrom(FROM_EMAIL);
helper.setTo(message.to());
helper.setSubject("[매일메일] " + message.subject());
helper.setText(message.text(), true);
return mimeMessage;
}
@Override
protected void handleSuccess(MailMessage message) {
mailEventRepository.save(MailEvent.success(message.to(), message.type()));
}
@Override
protected void handleFailure(MailMessage message) {
mailEventRepository.save(MailEvent.fail(message.to(), message.type()));
}
}
@Slf4j
@Component("questionSender")
public class QuestionSender extends AbstractMailSender<SubscribeQuestionMessage> {
private final SubscribeQuestionRepository subscribeQuestionRepository;
public QuestionSender(JavaMailSender javaMailSender, SubscribeQuestionRepository subscribeQuestionRepository) {
super(javaMailSender);
this.subscribeQuestionRepository = subscribeQuestionRepository;
}
@Override
protected void logSending(SubscribeQuestionMessage message) {
Subscribe subscribe = message.subscribe();
Question question = message.question();
QuestionCategory category = question.getCategory();
log.info("질문지를 전송합니다. email = {}, questionId = {}, subject = {}, category = {}",
subscribe.getEmail(), question.getId(), message.subject(), category.toLowerCase());
}
@Override
protected MimeMessage createMimeMessage(SubscribeQuestionMessage message) throws MessagingException {
Subscribe subscribe = message.subscribe();
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
mimeMessage.setHeader("X-SES-CONFIGURATION-SET", "my-first-configuration-set");
mimeMessage.setHeader("X-SES-MESSAGE-TAGS", "mail-open");
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, false, "UTF-8");
helper.setFrom(FROM_EMAIL);
helper.setTo(subscribe.getEmail());
helper.setSubject("[매일메일] " + message.subject());
helper.setText(message.text(), true);
return mimeMessage;
}
@Override
protected void handleSuccess(SubscribeQuestionMessage message) {
subscribeQuestionRepository.save(SubscribeQuestion.success(message.subscribe(), message.question()));
}
@Override
protected void handleFailure(SubscribeQuestionMessage message) {
subscribeQuestionRepository.save(SubscribeQuestion.fail(message.subscribe(), message.question()));
}
}
이렇게 하여 추상 클래스인 AbstractMailSender
클래스에 공통 로직이 집중되어 중복 코드를 제거했습니다. 그래서 공통 로직을 변경해야할 때 이 클래스만 변경하면 됩니다. 그리고 또 다른 종류의 메일 전송 클래스가 필요하면 이 클래스를 상속하여 쉽게 추가할 수 있게 됐습니다.
댓글남기기