오늘은 지난 시간에 저장한 질문들을 메일로 전송해보자.
Spring Boot의 메일 전송 라이브러리는 여러 가지가 있지만 spring-boot-starter-mail 의존성을 설치하고 JavaMailSender 인터페이스를 사용해 빠르게 개발하자. 서드 파티 라이브러리들이 있지만 메일 전송에 있어서 큰 차이가 없는 것 같고 JavaMailSender는 Spring에서 제공하는 API이기 때문에 서드 파티 라이브러리를 사용할 필요가 없다.
JavaMailSender
MIME 메시지를 전송할 수도 있고 SimpleMail을 전송할 수도 있다.
이 프로젝트에서는 MIME을 사용해 첨부파일을 사용하거나 HTML을 전송하지는 않을 것이기 때문에 SimpleMail을 사용하자.
메일 SMTP 서버는 Gmail의 SMTP 서버를 활용해 전송할 것인데 적용 방법은 밸덩 블로그 링크를 참고하기 바란다.
우리는 메일을 다운받는 기능을 작성할 필요는 없으니 SMTP만 있으면 된다.
SMTP는 메일을 전송하기 위한 프로토콜이고 POP3, IMAP은 SMTP 서버에서 메일 클라이언트로 메일을 다운로드 받기 위한 프로토콜
사용하는 클래스 및 인터페이스
- JavaMailSender - 인터페이스
- 메일을 전송하기 위한 인터페이스로 그 구현체인 JavaMailSenderImpl을 DI받아 사용한다.
- MailMessage - 인터페이스
- MimeMailMessage
- HTML, 첨부 파일 등이 포함된 메일을 전송하는 구현체
- SimpleMailMessage
- 간단한 텍스트를 전송하는 구현체
- MimeMailMessage
간단한 텍스트로 면접 질문을 전달할 수 있을 것이라 판단해 SimpleMailMessage를 사용하자.
@Service
public class QuestionMailService implements QuestionSendService {
private final JavaMailSender emailSender;
@Autowired
public QuestionMailService(JavaMailSender emailSender, MailMessage emailMessage) {
this.emailSender = emailSender;
this.emailMessage = emailMessage;
}
@Override
public void sendQuestions() {
SimpleMailMessage emailMessage = new SimpleMailMessage();
emailMessage.setTo("receiver@mail.com");
emailMessage.setSubject("title");
emailMessage.setText("This is text");
emailSender.send(emailMessage);
}
}
아무튼 이렇게 메일을 보내는 메서드를 실행하면 다음과 같이 메일이 잘 오는 것을 볼 수 있다.
진짜 구현
이제 메일 전송 라이브러리를 통해 메일을 전송해봤으니 진짜 우리의 비즈니스 로직인 사용자에게 메일을 전송하는 코드를 작성해야 한다.
- 특정 시간에 메일 받기를 요청한 멤버들을 대상으로 메일을 전송하자
- 먼저 해당 시간에 메일 전송을 요청한 사용자 리스트를 가져온 뒤 메일 전송한다.
@Override
public void sendQuestions() {
List<MemberResponse> targetMembers = memberService.getMembersByDesiredTime();
emailMessage.setTo(targetMembers.parallelStream().map(MemberResponse::email)
.toArray(String[]::new)); // "mail1@gmail.com" ,"mail2@gmail.com" 형식으로 전달됨
/* 기존의 메일 전송 코드 */
}
여기서 setTo 메서드의 경우 한 번에 여러 사용자에게 전송할 수 있도록 varargs 를 지원한다. 따라서 이 메서드를 사용해 결과를 보자.
나 뿐만 아니라 같이 보낸 다른 사람의 이메일도 같이 보인다..! 이러면 개인정보가 유출될 가능성이 있기 때문에 이 방법이 아니라 loop를 돌아 각각 메일을 송신하도록 변경해야 한다. 아래 코드에서 getMembersByDesiredTime() 메서드는 나중에 설명하므로 일단은 구조에만 집중하자.
List<MemberResponse> targetMembers = memberService.getMembersByDesiredTime();
targetMembers.parallelStream().forEach(members -> {
SimpleMailMessage emailMessage = new SimpleMailMessage();
emailMessage.setTo(members.email());
emailMessage.setSubject("title");
emailMessage.setText("This is text");
emailSender.send((SimpleMailMessage) emailMessage);
});
}
또한, 각각의 전송하는 로직이 이전 작업에 종속적이지 않기 때문에 병렬적으로 처리해도 된다. 따라서, parallelStream을 사용하여 병렬로 전송하자.
이전과 같이 다른 사람의 이메일은 보이지 않고 오직 ‘나에게’만 보이는 것을 알 수 있다.
이제 사용자가 선택한 시간에 맞춰 질문들을 메일로 전달하자
여기서 가정은 사용자가 1시간 단위로 메일 받기 원하는 시간을 설정한다는 것이다.
그리고 해당 시간을 원하는 사용자들을 조회해 메일 수신자로 지정한다. 아까 설명한다고 했던 getMembersByDesiredTime() 메서드의 구현 코드이다.
@Service
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
public List<MemberResponse> getMembersByDesiredTime() {
int currentHour = LocalDateTime.now().getHour();
List<Member> members = memberRepository.findMembersByDesiredTime(currentHour);
return members.stream().map(MemberResponse::from).toList();
}
}
이제 QuestionMailService는 완성되었다.
QuestionSender에서 send 메서드를 통해 메일을 전송해주면 된다. 하지만 설계 시 예상하지 못했던 문제가 있었다. 전송할 질문을 선택하는 책임을 MailService가 지기에는 부적절하다. 따라서 이를 분리하기 위해 QuestionPickService 등으로 만들어야 하고 질문을 문자열로 만들어 꾸미기 위한 Decorator 클래스도 필요해졌다.
설계를 변경해서 다시 그려보자
그리고 코드를 보자
@Component
public class QuestionSender {
private final QuestionSendService questionSendService;
private final QuestionPickService questionPickService;
private final QuestionDecorator questionDecorator;
public QuestionSender(QuestionSendService questionSendService,
QuestionPickService questionPickService, QuestionDecorator questionDecorator) {
this.questionSendService = questionSendService;
this.questionPickService = questionPickService;
this.questionDecorator = questionDecorator;
}
public void send() {
List<QuestionPickResponse> questionPickResponses = questionPickService.pickQuestions();
String decoratedQuestion = questionDecorator.decorateQuestion(questionPickResponses);
questionSendService.sendQuestions(decoratedQuestion);
}
}
이제 질문을 pick하고 decorate하고 send하는 구조가 완성되었다.
NewLineQuestionDecorator는 단순히 줄바꿈을 통해 질문의 가독성을 높이는 클래스이다. 추후 다른 데코레이터로 변경할 수 있도록 하기 위해 interface를 중간에 두었다.
QuestionSQLRandomPickService는 SQL 상에서 랜덤 질문을 뽑아내는 클래스이다. 추후 다른 방법으로 랜덤한 문제를 뽑아낼 수 있기 때문에 interface를 중간에 두었다.
@Component
public class NewLineQuestionDecorator implements QuestionDecorator{
@Override
public String decorateQuestion(List<QuestionPickResponse> questions) {
StringBuilder sb = new StringBuilder();
sb.append(getDefaultMessage());
questions.forEach(question -> {
sb.append("\u2022");
sb.append(question.content()).append("\n");
sb.append("category : ").append(question.category().name()).append("\n\n");
});
return sb.toString();
}
}
지역 변수 상에서의 StringBuilder는 스레드 세이프하기 때문에 Buffer로 하지 않았다.
@Component
public class QuestionSQLRandomPickService implements QuestionPickService {
private final QuestionRepository questionRepository;
private static final int PICK_COUNT = 3;
public QuestionSQLRandomPickService(QuestionRepository questionRepository) {
this.questionRepository = questionRepository;
}
public List<QuestionPickResponse> pickQuestions() {
List<Question> randomQuestions = questionRepository.findQuestionsRandomlyCount(
PICK_COUNT);
return randomQuestions.stream().map(QuestionPickResponse::from).toList();
}
}
이렇게 오늘의 구현을 마무리하고 결과 메일 이미지를 보자.
추가로 오늘의 삽질을 하나 올리자면 ... 즉, varargs를 파라미터로 가지는 메서드를 호출할 때 … 부분에 어떤 데이터를 넣어야 할 지 몰랐다. 물론 하드코딩으로 a, b, c 해도 되겠지만 이러면 유저를 변경할 수 없다.
이를 해결할 수 있는 방법은 Array다. 원소들을 Array[]로 변경해서 대입하면 된다.
참고
https://stackoverflow.com/questions/9863742/how-to-pass-an-arraylist-to-a-varargs-method-parameter
'프로젝트' 카테고리의 다른 글
Chat GPT 활용 서비스 구현하기 - 7. 회고 (0) | 2023.05.06 |
---|---|
Chat GPT 활용 서비스 구현하기 - 6. 스케쥴러 (0) | 2023.05.06 |
Chat GPT 활용 서비스 구현하기 - 4. 데이터 저장하기 (0) | 2023.05.04 |
Chat GPT 활용 서비스 구현하기 - 3. Open AI API 연동하기 ver2. (0) | 2023.05.02 |
Chat GPT 활용 서비스 구현하기 - 2. Open AI API 연동하기 (0) | 2023.05.02 |