피카소 경매 사이트는
사용자가 등록한 경매 작품의 상태값이 변경됨에 따라
사용자에게 메일을 보내는 서비스를 제공한다.
메일의 종류는 대략 9~10가지인데,
대표적으로는
- 경매 등록 성공 메일
- 경매 등록 미승인 메일
- 경매 작품 낙찰 메일
- 경매 작품 유찰 메일
- 내가 입찰한 경매 작품의 상황에 대한 메일
등등이 있다...
종류가 많다 보니 위 사진과 같은 화면처럼 메일을 발송하려면
HTML 템플릿화하여 관리해야 했다.
코드를 보면 알 수 있듯이
종류가 너무 많아 모두 구현하지는 못했다... ㅠ
메일 발송은 스케쥴링과 매우 밀접한 관련이 있는데
스케쥴링에 대해서는
이후에 따로 포스팅 할 예정이다.
아래 Github PR에 자세한 코드가 나와있다.
[feat] 메일 발송기능 구현 완료 by donsonioc2010 · Pull Request #33 · donsonioc2010/picasso
Mail 의존성 추가 메일 발송 기능 유틸리티 구현 메일 발송관련 샘플 Controller및 html추가 주의사항 : IntellijIDEA에서 SendMailUtil에서 JavaMailSender Bean을 못찾아서 뻘건줄이 떠도 정상 맞아요! @bongsh0112 @5
github.com
0) 메일 발송 관련 사전 설정
Email에 액세스 하기 위해
필자의 팀은 IMAP을 사용하기로 했다.
아래 링크는 Email에 액세스하는 대표적인 방법인
IMAP 및 POP에 대해 잘 서술되어 있다.
IMAP 및 POP 이란? - Microsoft 지원
구독 혜택을 살펴보고, 교육 과정을 찾아보고, 디바이스를 보호하는 방법 등을 알아봅니다. Microsoft 365 구독 혜택 커뮤니티를 통해 질문하고 답변하고, 피드백을 제공하고, 풍부한 지식을 갖춘
support.microsoft.com
IMAP 및 POP 관련 설정은 아래 더보기에 서술해놓았다.
0-1) Google IMAP 관련 설정
Google IMAP을 이용하기 위해,
관련 설정을 잡아주어야 한다.


POP와 IMAP 모두를 사용하여
휴대폰, 노트북 등의 전자 기기에서
어플리케이션에서 발송한 메일을 확인하게 한다.
필자가 이해한 바로는
IMAP은 전자 메일 서버에 메일의 내용을 저장하고
POP는 로컬에 메일을 저장하는 방식이라
하나의 방법만 사용해도 될 것 같았으나
현재 만드는 서비스가 MVP이기도 하고
시간도 많이 부족했기 때문에
안전하게 테스트용 메일을 구축하기 위해
두 가지 방법을 모두 체크했다.
0-2) 구글 앱 비밀번호 설정



구글에서 제공하는 메일 API를 사용하기 위해서는
구글 앱 비밀번호를 설정해주어야 한다.
이 정보는 VM option에 들어가야 하는 정보이니
생성된 앱 비밀번호는 기억하고 있자.
0-3) application.yml 파일 작성
spring:
mail:
host: smtp.gmail.com
port: 587
username: ${userEmail}
password: ${generatedPassword}
properties:
mail:
smtp:
auth: true
starttls:
enable: true
메일과 관련된 서비스이므로
포트는 SMTP에 맞게 587로 지정하고
username의 경우
메일 발송의 주체가 되는 사용자의 실제 구글 이메일을 적고
password의 경우
위 과정에서 생성된 비밀번호를 적으면 된다.
여기서 필자의 경우
이 정보들을 VM option에 넣으려 했고,
당연히 자동 배포 시에는
스크립트 파일에 -D 옵션으로 지정하여
프로그램을 실행시켰다.
1) 메일 발송 코드 작성
1-1) SendMailUtil
메일 발송에 관한 Util 파일은
Common 모듈에 작성되어 있다.
@Slf4j
@Component
@RequiredArgsConstructor
public class SendMailUtil {
private final JavaMailSender javaMailSender;
private final SpringTemplateEngine templateEngine;
/**
* Key는 현재 Mail에 사용중인 변수, Value는 해당 메일 페이지에 들어갈 값
*
* @param toUser 발송자 메일
* @param title 메일 제목
* @param pagePath ThymeLeaf(HTML)파일 경로
* @param content ThymeLeaf에 들어갈 변수 Map (Not Null!!!)
* @return
*/
public boolean sendMail(String toUser, String title, String pagePath, Map<String, Object> content) {
try {
parameterValidate(toUser, title, content);
MimeMessage message = javaMailSender.createMimeMessage();
message.addRecipients(Message.RecipientType.TO, toUser);
message.setSubject(title, UTF_8);
message.setText(getContent(pagePath, content), UTF_8, "html");
javaMailSender.send(message);
log.info("[SendMail Success] ToUser >>> {}, Title >>> {}, PagePath >>> {}", toUser, title, pagePath);
return true;
} catch (MessagingException e) {
log.error("[SendMail Failed] Exception Reason >>> MessageException, ToUser >>> {}, Title >>> {}, PagePath >>> {}", toUser, title, pagePath);
throw MailSendException.EXCEPTION;
} catch (Exception e) {
log.error("[SendMail Failed] Exception Reason >>> {}, ToUser >>> {}, Title >>> {}, PagePath >>> {}", e.getClass(), toUser, title, pagePath);
throw MailSendException.EXCEPTION;
}
}
// 메일 발송에 필요한 인자들을 검증하는 validator
private void parameterValidate(String toUser, String title, Map<String, Object> content) {
if (toUser == null || title == null || content == null ||
title.isEmpty() || toUser.isEmpty()) {
throw IllegalArgumentException.EXCEPTION;
}
}
// html template에 내용을 넣어 가져오는 메소드
private String getContent(String pagePath, Map<String, Object> content) {
Context context = new Context();
content.forEach(context::setVariable);
return templateEngine.process(pagePath, context);
}
}
JavaMailSender 객체를 주입받아
객체가 제공하는 메소드를 이용하여 메일을 발송한다.
pagePath에 들어갈 내용은
렌더링되어야 할 페이지와 밀접한 관련이 있기 때문에
Api 모듈에 상수로 정의되어 있다.
public class MailPathConstants {
public static final String SAMPLE_MAIL = "mail/sample";
// 관리자 미승인으로 인한 유찰메일 안내
public static final String PICTURE_NOT_APPROVE_REJECT_MAIL = "mail/not-approve-reject-mail";
// 입찰자가 없음으로 인한 유찰메일 안내
public static final String PICTURE_NO_BID_REJECT_MAIL = "mail/no-bid-reject-mail";
// 경매 낙찰 안내 메일
public static final String PICTURE_SUCCESSBID_MAIL = "mail/success-bid-picture-mail";
// 경매 시작 안내 메일
public static final String PICTURE_BIDDING_MAIL = "mail/bidding-picture-mail";
public static final String PICTURE_APPROVE_MAIL = "mail/approve-picture-mail";
}
이렇게 정의되어있는 thymeleaf html 파일들은
Api 모듈의 resources에 정의되어 있다.
각 html 파일에는 실제로 메일이 발송되어
수신자가 메일을 까보았을 때 나오는 화면이 작성되어있다.
이러한 메일 템플릿 html 파일은
String 형식으로 파싱되어 리턴된다...!
1-2) SendMailService
public void adminApproveMail(Picture picture) {
sendMailUtil.sendMail(
picture.getUser().getEmail(),
REJECT.getMailTitle(),
PICTURE_APPROVE_MAIL,
new HashMap<>() {{
put("pictureName", picture.getPictureName());
put("startBidDate", picture.getBidStartDate().toString());
put("link", picassoProperties.getDomain() + "pictures/" + picture.getPictureId());
}}
);
}
실제로 메일을 발송하는 코드이다.
비즈니스 로직에서 활용될 Service 코드이기에
Api 모듈에서 작성되었다.
위 단계에서 sendMail의 파라미터에 무엇이 들어갈지 헷갈렸다면
이 코드에서 궁금증이 해결되었을 것이다.
다른 종류의 메일 발송에 대해서는
Github 코드를 참고하자.
2) 메일 발송 테스트
스케쥴링을 이용하여 상태가 바뀐 경매 작품에 대해
유찰되었다는 알림 메일이 자동으로 발송된 것을 확인했다.
여기서 문제는,
메일 발송 시 한 건 당 500~1000ms의 발송 시간이 필요한데
수천건의 스케쥴링을 할 때 상태값 변화에 대해
메일을 발송한다면
한 작품의 상태값 변화 후
다음 작품에 대해 변화를 시키기까지
꽤 오랜 시간 지연이 발생할 것이다.
이것은 거진 서비스 장애의 문제이므로,
이후 포스트에서 비동기를 활용하여
이 문제를 해결하는 과정을 서술해보려 한다.
'🌿 Spring' 카테고리의 다른 글
[Spring] 전략 패턴 사용하기 (0) | 2024.01.29 |
---|---|
메일 발송 - 스케쥴링 간 문제 해결 with Async (1) | 2023.10.03 |
등록된 미술품 상태 변경 with Scheduling (0) | 2023.10.03 |
무한스크롤 API 구현하기 (1) | 2023.09.04 |