[Trouble Shooting/성능 개선] 비동기 작업 통한 단체 이메일 전송 성능 개선 ( + @Async)

2025. 1. 19. 22:13· Trouble Shooting
목차
  1. 1. 문제 상황 파악
  2. 1-1. 전송 시간 측정 결과
  3. 2. 문제 원인 파악
  4. 2-1. 기존 이메일 전송 관련 서비스 단 코드
  5. 3. 해결 방법 및 결과 검증
  6. 3-1. 비동기 처리
  7. 3-2. 프로젝트에 적용

1. 문제 상황 파악

  • 프로젝트에 쿠폰을 보유하지 않은 회원 목록에 대해 이메일 전송 기능을 구현함
  • 샘플 유저 데이터 20개를 삽입 후 이메일 전송 기능을 테스트했을 때 시간이 꽤 걸리는 것을 확인
    ⇒ 실제 소요 시간을 찍어보기로 함

 

1-1. 전송 시간 측정 결과

  • 구현한 api는 이메일 주소 1개에 대해 이메일을 전송하는 기능을 가짐
  • 따라서 api 호출만의 소요 시간으로는 여러개 이메일 전송에 걸리는 시간을 측정하기 어려울 것이라 판단
    ⇒ 현재 FE에서 비동기적으로 api를 호출하므로 총 시간을 브라우저의 콘솔에 출력해보기로 함
더보기

FE 코드 일부

 

// 이메일 전송 함수 일부

try {
    await Promise.all(selectedEmails.map(async (email, index) => {
        const emailTemplate = CouponEmailTemplate({ coupon: coupon }); // 이메일 템플릿 HTML 가져오기

        // 실제 이메일 전송 요청
        const response = await axios.post('http://localhost:8080/api/admin/coupons/email', {
            couponId: coupon.id,
            address: email,
            template: emailTemplate // 결합된 템플릿 사용
        }, {
            headers: {
                'Content-Type': 'application/json',
                'Authorization': `Bearer ${localStorage.getItem('token')}`
            }
        });

        if (response.status !== 200) {
            throw new Error(`이메일 전송에 실패했습니다: ${email}`);
        }

        // 진행률 업데이트
        setProgress(((index + 1) / selectedEmails.length) * 100);
        await new Promise(resolve => setTimeout(resolve, 50)); // 요청 사이에 잠시 대기
    }));

    alert('모든 쿠폰 이메일 전송을 완료했습니다!');
    onRequestClose(); // 모달 닫기
} catch (err) {
    try {
        await sendRefreshTokenAndStoreAccessToken();
        window.location.reload();
    } catch (e) {
        console.error(err.message);
    }
} finally {
    setSending(false);
    const endTime = Date.now(); // 종료 시간 기록
    const duration = endTime - startTime; // 걸린 시간 계산
    console.log('전송한 이메일 개수: ' + selectedEmails.length + '개');
    console.log(`이메일 전송에 걸린 시간: ${duration}ms`);
}
};
  • Promise.all, async / await 를 사용하여 선택된 이메일 목록에 대해 비동기적으로 api 호출하는 코드
  • 시작과 끝의 시간을 기록해 걸린 시간을 계산해 콘솔 창에 출력하는 코드 추가함

 

  • 이메일 20개 전송 시 소요 시간 측정

⇒ 20개 이메일 전송 시 18.871s (= 18871ms) 소요됨을 확인

⇒ 만약 이메일을 전송해야 하는 유저가 20명이 아닌 1만명, 10만명 … 이라면 시간이 매우 많이 소요될 것이라고 판단해 성능 개선하려고 함!

 

2. 문제 원인 파악

2-1. 기존 이메일 전송 관련 서비스 단 코드

@Service
@RequiredArgsConstructor
public class CouponEmailService {

    private final JavaMailSender javaMailSender;

    public void sendEmail(CouponEmailRequestDto dto) {

        MimeMessage mimeMessage = javaMailSender.createMimeMessage();

        try{
            MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, false, "UTF-8");

            // 메일을 받을 수신자 설정
            mimeMessageHelper.setTo(dto.getAddress());

            // 메일의 제목 설정
            mimeMessageHelper.setSubject("FITinside 쿠폰 메일");

            // 발신자 설정
            mimeMessageHelper.setFrom("chm20060@gmail.com", "FITinside 관리자");

            // 메일의 내용 설정
            mimeMessageHelper.setText(dto.getTemplate(), true);

            javaMailSender.send(mimeMessage);
        } catch (Exception e) {
            throw new CustomException(ErrorCode.INVALID_EMAIL_DATA);
        }
    }
}

⇒ 특별한 비동기 처리나 멀티스레딩 기능이 적용되지 않은 상태이므로 단일 스레드로 실행됨!

📌 단일 스레드에서 다수의 이메일 전송을 순차적으로 처리할 때의 문제점

- 단일 스레드에서는 한 가지 작업이 실행 ~ 완료 될 때 까지 다른 작업을 할 수 없음
- 따라서 첫 번째 이메일 전송이 완료된 후에야 두 번째 이메일 전송이 시작됨
- 즉, 대략적으로 (한 개의 이메일을 보내는 시간) * (이메일 목록의 수) 만큼의 소요시간이 걸릴 것임!

 

3. 해결 방법 및 결과 검증

3-1. 비동기 처리

  • 특정 작업을 별도의 스레드에서 수행하게 함
    ⇒ 애플리케이션의 반응성을 높일 수 있음
    ⇒ 메인 스레드의 블로킹 피할 수 있음
  • 소요 시간이 긴 작업 등을 메인 애플리케이션의 흐름과 분리할 수 있음!

 

3-1-1. 스프링에서의 비동기 처리

1. @Async 어노테이션

  • 비동기 메서드 정의 시 사용
  • 해당 어노테이션이 붙은 메서드는 호출 시 새로운 스레드에서 실행됨
@Async
public void asyncMethod() {
		
    // 생략 ...
		
}

 

2. Executor

  • 비동기 작업을 처리할 스레드 풀 설정
  • 스프링은 기본적으로 SimpleAsyncTaskExecutor 사용함
@Configuration
@EnableAsync  // 기본적으로 SimpleAsyncTaskExecutor 사용됨
public class AsyncConfig {
}

 

  • ThreadPoolTaskExecutor
    • 기존의 SimpleAsyncTaskExecutor는 스레드 풀을 사용하지 않으므로 매번 새로운 스레드 생성함
      ⇒ 성능이 저하될 수 있음!
    • ThreadPoolTaskExecutor 사용해 스레드 풀 설정 가능
      ⇒ 비동기 메서드 호출 시 스레드 풀에서 스레드를 가져와 작업 처리하게 되어 성능, 자원 관리 측면에서 더 효율적일 수 있음!
@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean
    public ThreadPoolTaskExecutor taskExecutor() {	// ThreadPoolTaskExecutor 사용
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2); // 최소 스레드 수
        executor.setMaxPoolSize(5); // 최대 스레드 수
        executor.setQueueCapacity(100); // 대기열 크기
        executor.initialize();
        return executor;
    }
}

 

3-2. 프로젝트에 적용

3-2-1. AsyncConfig 생성

@Configuration
@EnableAsync
public class AsyncConfig {

    @Bean
    public ThreadPoolTaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2); // 최소 스레드 수
        executor.setMaxPoolSize(10); // 최대 스레드 수
        executor.setQueueCapacity(100); // 대기열 크기
        executor.initialize();
        return executor;
    }
}

  • 확장 가능성을 염두에 두고 더 효율적인 관리를 위해 ThreadPoolTaskExecutor 설정함

 

3-2-2. @Async 어노테이션 추가

@Service
@RequiredArgsConstructor
public class CouponEmailService {

    private final JavaMailSender javaMailSender;

    @Async  // 비동기 작업을 위한 어노테이션 추가
    public void sendEmail(CouponEmailRequestDto dto) {

        MimeMessage mimeMessage = javaMailSender.createMimeMessage();

        try{
            MimeMessageHelper mimeMessageHelper = new MimeMessageHelper(mimeMessage, false, "UTF-8");

            // 메일을 받을 수신자 설정
            mimeMessageHelper.setTo(dto.getAddress());

            // 메일의 제목 설정
            mimeMessageHelper.setSubject("FITinside 쿠폰 메일");

            // 발신자 설정
            mimeMessageHelper.setFrom("chm20060@gmail.com", "FITinside 관리자");

            // 메일의 내용 설정
            mimeMessageHelper.setText(dto.getTemplate(), true);

            javaMailSender.send(mimeMessage);
        } catch (Exception e) {
            throw new CustomException(ErrorCode.INVALID_EMAIL_DATA);
        }
    }
}
  • @Async 어노테이션을 추가해 비동기 작업이 가능하게 함

 

3-3. 성능 개선 결과 확인

  • 기존과 동일하게 20개의 이메일 주소에 이메일을 전송해 비교 진행

 

⇒ 20개 이메일 전송에 571ms 소요됨을 확인

⇒ 기존 18871ms 대비, 약 96.97% 의 성능 개선을 확인 (더 많은 이메일로 비교한다면 더 큰 성능 개선을 이룰 것으로 예상!)

저작자표시 변경금지 (새창열림)

'Trouble Shooting' 카테고리의 다른 글

[Trouble Shooting/성능 개선] Redis로 캐싱 도입해 채팅 전송 및 조회 성능 개선  (1) 2025.01.03
[Trouble Shooting/성능 개선] QueryDSL 로 no-offset 페이지네이션 구현하여 조회 성능 개선  (0) 2024.12.31
[Trouble Shooting] JPA 사용 시 발생한 N+1 문제를 fetch join을 사용한 JPQL을 작성하여 해결  (0) 2024.12.30
[Trouble Shooting] 단위 테스트 작성 시 발생한 JpaAuditingHandler 빈 생성 오류 해결  (1) 2024.12.30
  1. 1. 문제 상황 파악
  2. 1-1. 전송 시간 측정 결과
  3. 2. 문제 원인 파악
  4. 2-1. 기존 이메일 전송 관련 서비스 단 코드
  5. 3. 해결 방법 및 결과 검증
  6. 3-1. 비동기 처리
  7. 3-2. 프로젝트에 적용
'Trouble Shooting' 카테고리의 다른 글
  • [Trouble Shooting/성능 개선] Redis로 캐싱 도입해 채팅 전송 및 조회 성능 개선
  • [Trouble Shooting/성능 개선] QueryDSL 로 no-offset 페이지네이션 구현하여 조회 성능 개선
  • [Trouble Shooting] JPA 사용 시 발생한 N+1 문제를 fetch join을 사용한 JPQL을 작성하여 해결
  • [Trouble Shooting] 단위 테스트 작성 시 발생한 JpaAuditingHandler 빈 생성 오류 해결
공대생안씨
공대생안씨
전자공학과 학부생의 코딩 일기
티스토리
|
로그인
공대생안씨
공대생의 코딩 일기
공대생안씨
글쓰기
|
관리
전체
오늘
어제
  • All Categories (153)
    • Spring Boot (46)
      • JPA (7)
      • Lombok (2)
    • Java (21)
    • DevOps (3)
      • CI,CD (8)
      • Monitoring (2)
    • Database (7)
      • MySQL (5)
      • MongoDB (1)
      • H2 (1)
    • Trouble Shooting (5)
    • FE (4)
    • IntelliJ (3)
    • Git (3)
    • Algorithm (41)

블로그 메뉴

  • 홈
  • 태그
  • Github

공지사항

인기 글

hELLO · Designed By 정상우.v4.2.2
공대생안씨
[Trouble Shooting/성능 개선] 비동기 작업 통한 단체 이메일 전송 성능 개선 ( + @Async)
상단으로

티스토리툴바

개인정보

  • 티스토리 홈
  • 포럼
  • 로그인

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.