비동기로 이메일 발송 구현

이동영·2025년 12월 1일

웹개발

목록 보기
28/37
post-thumbnail

이번 프로젝트에서 "관리자가 문의에 답변을 달면, 작성자에게 이메일로 알림을 보내주는 기능"을 구현해야 했다.
단순히 JavaMailSender를 써서 메일을 보내는 건 어렵지 않다. 하지만 그렇게 하면 UX에 부정적인 영향을 주게 된다.

메일 보내기의 문제점

초기 구현은 간단했다.

  1. 관리자가 [답변 등록] 버튼을 누른다.
  2. 서버는 DB에 답변을 저장한다. (0.01초)
  3. 서버는 구글(Gmail) 서버에 접속해서 메일을 보낸다. (3~5초)
  4. 메일 전송이 끝나야 관리자에게 "등록되었습니다" 응답을 보낸다.

무엇이 문제인가?

관리자는 버튼을 누르고 3~5초 동안 화면이 멈춘 채(로딩) 기다려야 한다.
게다가 만약 구글 서버가 일시적으로 느리거나 터지면, 우리 서버의 답변 등록 기능까지 덩달아 실패(Rollback)하거나 타임아웃이 걸린다.
메일 발송은 부가 기능일 뿐인데, 이것 때문에 핵심 기능(답변 저장)이 느려지거나 죽는 건 주객전도다.

비동기로 해결

이 문제를 해결하는 열쇠는 비동기 처리(Asynchronous Processing)다.
메인 스레드는 주문만 처리하고, 메일 발송 같은 오래 걸리는 일은 보조 스레드에게 "이거 좀 보내놔" 하고 던져버린 뒤 즉시 응답을 주는 방식이다.

Spring Boot에서는 @Async 어노테이션 하나로 아주 쉽게 구현할 수 있다.

구현 과정

1. 의존성 추가 및 SMTP 설정

먼저 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

2. 비동기 활성화 (@EnableAsync)

스프링에게 "비동기 기능 쓸 거야"라고 알려줘야 한다. 메인 클래스에 어노테이션을 붙이자.

@EnableAsync // 이게 없으면 @Async를 붙여도 동기로 작동함!
@SpringBootApplication
public class DungeongApplication {
    public static void main(String[] args) {
        SpringApplication.run(DungeongApplication.class, args);
    }
}

3. 이메일 서비스 구현 (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());
        }
    }
}

4. 비즈니스 로직 연동

이제 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; 
}

결과 및 결론

Before (동기)

  • 동작 : 저장 -> (3초 대기) -> 메일 발송 -> 응답
  • 체감 : 버튼 누르고 한참 멍하니 있어야 함.

After (비동기)

  • 동작 : 저장 -> (메일은 백그라운드 스레드로 던짐) -> 즉시 응답
  • 체감 : 버튼 누르자마자 "등록되었습니다!"

이메일, 푸시 알림, 로그 전송 같이 "실패해도 핵심 기능엔 지장이 없으면서 시간이 오래 걸리는 작업"은 무조건 비동기로 빼자.
개발자에게는 비록 @Async 한 줄이지만, 사용자에게는 3초의 시간을 선물하는 것이다.

0개의 댓글