
이번 프로젝트에서 "관리자가 문의에 답변을 달면, 작성자에게 이메일로 알림을 보내주는 기능"을 구현해야 했다.
단순히 JavaMailSender를 써서 메일을 보내는 건 어렵지 않다. 하지만 그렇게 하면 UX에 부정적인 영향을 주게 된다.
초기 구현은 간단했다.
관리자는 버튼을 누르고 3~5초 동안 화면이 멈춘 채(로딩) 기다려야 한다.
게다가 만약 구글 서버가 일시적으로 느리거나 터지면, 우리 서버의 답변 등록 기능까지 덩달아 실패(Rollback)하거나 타임아웃이 걸린다.
메일 발송은 부가 기능일 뿐인데, 이것 때문에 핵심 기능(답변 저장)이 느려지거나 죽는 건 주객전도다.
이 문제를 해결하는 열쇠는 비동기 처리(Asynchronous Processing)다.
메인 스레드는 주문만 처리하고, 메일 발송 같은 오래 걸리는 일은 보조 스레드에게 "이거 좀 보내놔" 하고 던져버린 뒤 즉시 응답을 주는 방식이다.
Spring Boot에서는 @Async 어노테이션 하나로 아주 쉽게 구현할 수 있다.
먼저 build.gradle에 메일 라이브러리를 추가한다.
implementation 'org.springframework.boot:spring-boot-starter-mail'
그리고 application.properties (또는 yml)에 Gmail SMTP 설정을 잡는다.
(주의: 비밀번호는 구글 계정 비번이 아니라, 2단계 인증 후 발급받은 '앱 비밀번호'를 써야 한다.)
spring.mail.host=smtp.gmail.com
spring.mail.port=587
spring.mail.username=dreamshowchoir0524@gmail.com
# 구글 2단계 인증 후 발급받은 '앱 비밀번호' 16자리
spring.mail.password=xxxx xxxx xxxx xxxx
# SMTP 세부 설정
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
# 타임아웃 설정 (서버가 무한 대기하는 것 방지 / 단위: ms)
spring.mail.properties.mail.smtp.connectiontimeout=5000
spring.mail.properties.mail.smtp.timeout=5000
spring.mail.properties.mail.smtp.writetimeout=5000
스프링에게 "비동기 기능 쓸 거야"라고 알려줘야 한다. 메인 클래스에 어노테이션을 붙이자.
@EnableAsync // 이게 없으면 @Async를 붙여도 동기로 작동함!
@SpringBootApplication
public class DungeongApplication {
public static void main(String[] args) {
SpringApplication.run(DungeongApplication.class, args);
}
}
EmailService)메일 발송 메서드 위에 @Async를 붙여준다. 이제 이 메서드는 호출 즉시 별도의 스레드에서 돌아간다.
@Slf4j
@Service
@RequiredArgsConstructor
public class EmailService {
private final JavaMailSender javaMailSender;
@Async // 별도 스레드에서 실행됨
public void sendEmail(String to, String subject, String content) {
try {
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, false, "UTF-8");
helper.setTo(to);
helper.setSubject(subject);
helper.setText(content, true); // HTML 포맷 지원
javaMailSender.send(mimeMessage);
log.info("이메일 발송 성공: {}", to);
} catch (MessagingException e) {
// 비동기이므로 여기서 에러가 나도, 메인 로직(답변 저장)에는 영향이 없음
log.error("이메일 발송 실패: {}", e.getMessage());
}
}
}
이제 Service(사용할 서비스)에서 호출만 하면 된다. 필자는 문의 기능에 넣었기 때문에, InquiryService에서 호출하였다.
@Transactional
public void replyToInquiry(Long inquiryId, String answer) {
// 답변 저장 (핵심 로직) - 0.01초 소요
Inquiry inquiry = inquiryRepository.findById(inquiryId).orElseThrow();
inquiry.addAnswer(answer);
// 이메일 발송 (비동기 호출) - 여기서 대기하지 않고 바로 넘어감!
emailService.sendEmail(inquiry.getEmail(), "답변이 등록되었습니다", answer);
// 즉시 리턴
return;
}
이메일, 푸시 알림, 로그 전송 같이 "실패해도 핵심 기능엔 지장이 없으면서 시간이 오래 걸리는 작업"은 무조건 비동기로 빼자.
개발자에게는 비록 @Async 한 줄이지만, 사용자에게는 3초의 시간을 선물하는 것이다.