노약자를 위한 AI 말동무 서비스, POPPET 서비스의 개발 일대기입니다.
사용자에게 노약자가 특정 기간동안 AI와 대화한 내용의 요약본을 이메일로 전송하는 기능을 구현했습니다.
java sender 기능을 사용하기 위해 아래와 같이 의존성을 추가해줍니다
dependencies {
...
implementation 'org.springframework.boot:spring-boot-starter-mail'
...
}
spring:
mail:
host: smtp.gmail.com
port: 587
username: ${MAIL_USERNAME}
password: ${MAIL_PASSWORD}
properties:
mail:
smtp:
auth: true
timeout: 5000
ssl: false
starttls:
enable: true
required: true
💡gmail은 465포트(SSL)와 587포트(TLS)를 분리하여 제공
465포트 → ssl : true
587포트 → starttls.enable : true (+ ssl : false)
기능 흐름은 다음과 같습니다
ChatRoom
Entity의 is_mail_sent
필드 활용 (boolean)ChatRoom
을 생성하는 과정에서 User
의 이메일 전송 설정 주기를 활용해 생성하므로, 채팅방 선정 과정에서는 이메일 전송 설정 주기를 고려하지 않았음ChatRoom
Entity의 summary
필드 활용 (String)ChatRoom
의 is_mail_sent
필드 true
로 변경💡 왜 이벤트 기반으로 기능을 구현했는가?
1.is_mail_sent
필드 업데이트를 위해 해당 함수는Transaction
내부에서 수행되어야 함
2. 그러나, 이메일 전송과 같은 시간이 걸리고 외부 API를 호출해야 하는 작업을Transaction
내부에서 수행하는 것은 안티패턴
3. 따라서, DB 작업이 완료되고, 즉. 커밋이 완료된 후 외부 작업을 수행하도록 로직을 구현
→ 커밋 후 이벤트를 발행하면 리스너가 이벤트를 받아 수행하는 패턴
/**
* 유저의 이메일 전송 주기에 따라, 주기 내에 생성된 이메일의 요약 내용을 이메일로 전송한다.
*/
@Transactional
@Override
public void sendEmail(String username) {
// TODO: user data 얻는 과정 수정
User user = getUser(username);
// 가장 최근 생성되고 메일을 보내지 않은 채팅방 조회
List<ChatRoom> chatRooms = chatRoomRepository.findByUsernameAndCreatedAtAndIsMailSent(username);
if (chatRooms.isEmpty()) return;
// 메일 보낼 채팅방 요약 내용 추출
ChatRoom chatRoom = chatRooms.get(0);
String chatSummary = chatRoom.getSummary();
if (chatSummary == null) return;
// 메일 보냄 여부 수정
chatRoom.updateIsMailSent();
// 메일 전송 이벤트 발행
applicationEventPublisher.publishEvent(new EmailSendEvent(user, chatRoom));
}
EmailSendEvent
: 이벤트 정보를 저장하기 위한 클래스EmailSendEventListener
: 이벤트를 받아 처리하는 리스너@Async
를 이용해 비동기로 처리@EnableAsync
를 붙인 config 파일이 필요함 @TransactionalEventListener
: 트랜잭션 상태에 따라 이벤트를 수행할 수 있게 해주는 리스너DB 반영 이후 이메일이 전송되는 것을 보장하기 위해, AFTER_COMMIT
을 사용 → 이메일 재전송, 중복 방지
phase 값 | 실행 시점 |
---|---|
BEFORE_COMMIT | 트랜잭션 커밋 전에 실행됨 |
AFTER_COMMIT | 트랜잭션 커밋 후 실행됨 |
AFTER_ROLLBACK | 트랜잭션이 롤백된 경우에 실행 |
AFTER_COMPLETION | 커밋이든 롤백이든 트랜잭션 종료 후 실행 |
@Getter
public class EmailSendEvent {
private final User user;
private final ChatRoom chatRoom;
public EmailSendEvent(User user, ChatRoom chatRoom) {
this.user = user;
this.chatRoom = chatRoom;
}
}
@Slf4j
@Component
@RequiredArgsConstructor
public class EmailSendEventListener {
private final EmailSendService emailSendService;
@Async
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleEmailSendEvent(EmailSendEvent event) {
User user = event.getUser();
String summary = event.getChatRoom().getSummary();
for (Email email : user.getEmails()) {
try {
emailSendService.sendEmail(email.getEmailAddress(), summary);
} catch (Exception e) {
log.error("[*] {}으로 이메일 전송 중 오류 발생", email.getEmailAddress(), e);
}
}
}
}
진행 흐름은 다음과 같습니다
JavaMailSender
interface DIMimeMessage
인터페이스 채택mailSender.send
를 통해 이메일 전송메일을 보내는 과정에서 생기는 Exception은 emailSendService
를 호출하는 EmailSendEventListener
에서 catch하고 있기 때문에 별도의 catch문은 사용하지 않았습니다
@Slf4j
@Service
@RequiredArgsConstructor
public class EmailSendService {
private final JavaMailSender mailSender;
public void sendEmail(String to, String body) throws MessagingException {
MimeMessage mimeMessage = mailSender.createMimeMessage();
// 이메일 제목
String subject = "POPPET";
MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage);
mimeMessageHelper.setTo(to);
mimeMessageHelper.setSubject(subject);
mimeMessageHelper.setText(body, true);
mailSender.send(mimeMessage);
}
}
위 기능에 따라 이메일이 발송되면, 사용자는 다음과 같은 화면을 마주합니다.
여기에 템플릿을 적용해 예쁘게 이메일을 받아볼 수 있도록 추가적인 기능을 추가하겠습니다.
👇 다음 포스트 (템플릿 적용기) 👇
https://velog.io/@dooo_it_ly/SpringBoot-ApachePdfBox로-PDF에-글씨-그림-그리기