[Spring] - 이메일 전송 비동기 처리

CodeByHan·2025년 4월 13일

스프링

목록 보기
21/33

이메일 전송은 외부 SMTP 서버와의 네트워크 통신, I/O 처리 등 여러 과정을 거치기 때문에 서버 자원을 많이 사용하고, 특히 동기 방식으로 처리할 경우 사용자 응답 지연 등 전체 시스템 성능에 부정적인 영향을 줄 수 있는 무거운 작업이다.

📌 문제 발생


단순하게 동기적으로 이메일 전송

@Component
@RequiredArgsConstructor
public class EmailSender {

    private final JavaMailSender mailSender;

    public void sendEmail(String to, String subject, String text) {
        SimpleMailMessage message = new SimpleMailMessage();
        message.setTo(to);
        message.setSubject(subject);
        message.setText(text);
        mailSender.send(message);
    }
}

성능 테스트

SMTP 서버의 로그인 제한에 걸리지 않도록 너무 많은 요청을 보내지 않기 위해, 낮은 숫자의 가상 사용자를 사용했다. 실제로 많은 요청을 보내면 "Too many login attempts" 오류가 발생하여 SMTP 서버가 차단을 했다.

import http from "k6/http";
import { check, sleep } from "k6";

export let options = {
    stages: [
      { duration: "10s", target: 3 },   // 10초 동안 3명까지 증가
      { duration: "20s", target: 3 },   // 20초 동안 3명 유지
      { duration: "10s", target: 0 }    // 10초 동안 0으로 감소
    ],
    thresholds: {
      http_req_duration: ['p(95)<1000'], // 1초 이내
      http_req_failed: ['rate<0.1'],     // 실패율 10% 미만으로 일단 설정 완화
    },
  };
  
  

export default function () {
  const url = 'http://localhost:8080/api/email/sendCode';
  
  // EmailDto에 해당하는 JSON payload 생성
  const payload = JSON.stringify({
    email: "medewoh372@naobk.com" // 실제 테스트용 이메일 주소 사용
  });

  const params = {
    headers: {
      "Content-Type": "application/json",
    },
  };

  let res = http.post(url, payload, params);
  check(res, {
    "응답 상태가 202(ACCEPTED)인지": (r) => r.status === 202,
  });

  sleep(1);
}

시나리오 요약

  • 10초: 가상 사용자를 0에서 3명까지 점진적으로 증가

  • 20초: 3명의 가상 사용자를 유지

  • 10초: 가상 사용자를 3명에서 0명으로 감소

결과

  • 초당 요청 수 (Requests per Second): 평균 1.40, 최대 2.
  • 평균 응답 시간: 3.90초
  • 최대 응답 시간: 4.58초
  • 중앙값 응답 시간: 3.93초
  • 최소 응답 시간: 3.29초
  • 90% 이하 응답 시간: 4.26초
  • 95% 이하 응답 시간: 4.50초
  • HTTP 요청 차단 시간: 차단 시간이 최대 약 8.19초까지 기록

문제점 파악

  • 현재 평균 응답 시간이 약 3.90초로 측정되며, 최대 응답 시간도 4.58초에 달하는 것을 보면, 클라이언트 입장에서는 웹 어플리케이션 사용 시 상당한 지연을 체감할 가능성이 매우 크다.
  • 이메일 전송과 같은 부수 작업이 동기적으로 처리됨으로써 전체 요청 응답 시간이 늦어지고, 이로 인해 사용자는 페이지 로딩이나 작업 완료 시 길어지는 대기 시간을 직접적으로 경험하게 될 것이다. 클라이언트는 이러한 긴 응답 시간을 느끼며 전반적으로 체감 성능 저하를 경험할 가능성이 높다.

❓ 고민

  • 초기에는 별도의 비동기 큐(예: RabbitMQ와 같은)를 도입하는 방안을 고민했지만, 이메일 전송과 같은 간단한 로직에 요구 사항에 비해 너무 복잡하고 과하고 시간이 오래걸릴 것이라고 판단
  • Spring의 @Async를 활용해 이메일 전송 작업을 비동기 처리하는 방식을 선택

개선 시작

비동기 작업 설정

@EnableAsync
@Configuration
public class AsyncConfig {

    private static final int CORE_POOL_SIZE = 10;
    private static final int MAX_POOL_SIZE = 50;
    private static final int QUEUE_CAPACITY = 20;

    @Bean(name = "emailTaskExecutor")
    public Executor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(CORE_POOL_SIZE);
        executor.setMaxPoolSize(MAX_POOL_SIZE);
        executor.setQueueCapacity(QUEUE_CAPACITY);
        executor.setThreadNamePrefix("Async-");

        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }

}
  • @EnableAsync : Spring 애플리케이션에서 @Async 어노테이션을 사용할 수 있게 활성화
  • CORE_POOL_SIZE (10) : 스레드 풀의 기본 스레드 개수. 즉, 시스템이 최소 10개의 스레드를 유지하며, 이 스레드들이 기본적으로 작업을 처리
  • MAX_POOL_SIZE (50) : 작업 부하가 많을 경우, 스레드 풀은 최대 50개까지의 스레드를 생성하여 처리 가능
  • QUEUE_CAPACITY (20) : 코어 스레드들이 모두 바쁠 때, 들어오는 작업들을 임시로 보관할 수 있는 큐의 용량입니다. 큐에 20개의 작업이 저장될 수 있으며, 이 한도를 넘으면 스레드 풀이 최대 스레드 수를 넘어가지 않는 한 새로운 스레드를 생성해 작업을 처리
  • setThreadNamePrefix("Async-") : 스레드의 이름 접두사 설정
  • setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()) : 대기 큐가 꽉 차고 스레드 풀이 최대치로 동작 중인 경우, 새로 들어온 작업을 거부하지 않고 호출한 스레드(caller thread)가 대신 실행하도록 하는 정책

적용

@Component
@RequiredArgsConstructor
public class EmailSender {

    private final JavaMailSender mailSender;

    @Async("emailTaskExecutor")
    public void sendEmail(String to, String subject, String text) {
        SimpleMailMessage message = new SimpleMailMessage();
        message.setTo(to);
        message.setSubject(subject);
        message.setText(text);
        mailSender.send(message);
    }

}

성능 테스트

시나리오 요약

10초: 가상 사용자를 0에서 3명까지 점진적으로 증가

20초: 3명의 가상 사용자를 유지

10초: 가상 사용자를 3명에서 0명으로 감소

항목이메일 전송 동기 (초)이메일 전송 비동기 (초)개선율 (%)
평균 응답 시간3.900.016299.58%
최대 응답 시간4.580.1149497.49%
최소 응답 시간3.290.0059199.82%
90% 응답 시간 이하4.260.022299.48%
95% 응답 시간 이하4.500.02599.44%
HTTP 요청 차단 시간8.190.12898.44%
  • 모든 항목에서 두 번째 테스트의 성능이 첫 번째 테스트 대비 최소 97.49% 이상 개선
  • 특히 평균 및 최소 응답 시간에서 약 99.58%~99.82%의 개선율을 보여 서버의 응답 성능이 크게 향상
profile
노력은 배신하지 않아 🔥

0개의 댓글