비즈니스 요구사항이 빈번히 변경되는 지점에 추상화를 적용하여 클라이언트 코드 변경 전파 최소화

4 분 소요

요약

유사한 성격의 메일을 발송하는 두 클래스를 추상 클래스를 선언하고 템플릿 메서드 패턴을 사용하여 공통 로직을 한 곳으로 모아서 중복을 제거했습니다.

구독자에게 메일을 보내는 종류는 크게 두 가지가 있고 객체가 분리되어 있습니다. 첫 번째는 공지, 인증, 관리자 보고 등의 메일을 발송하는 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 클래스에 공통 로직이 집중되어 중복 코드를 제거했습니다. 그래서 공통 로직을 변경해야할 때 이 클래스만 변경하면 됩니다. 그리고 또 다른 종류의 메일 전송 클래스가 필요하면 이 클래스를 상속하여 쉽게 추가할 수 있게 됐습니다.

댓글남기기