티스토리 뷰

1. 성능 테스트를 위한 Mock 이메일 서버 설계

 

프로젝트에서는 Gmail SMTP 서버를 이용해 이메일 발송 기능을 구현했습니다.

하지만 부하 테스트를 진행하기 위해 실제 Gmail SMTP 서버를 사용할 경우 몇 가지 제약이 발생했습니다.

 

1. 발송 횟수 제한

Gmail 무료 계정은 하루 500회의 발송 제한이 있습니다. 이로 인해 대규모 부하 테스트를 충분히 수행하기 어려웠습니다. 

 

2. Rate Limit의 유동성

Gmail의 초당 처리 제한(Rate Limit)은 사용량에 따라 동적으로 변경됩니다. 이 특성은 성능 테스트 결과를 왜곡할 가능성을 높였습니다.

 

 

 

위와 같은 문제를 해결하기 위해 Gmail 대신 Mock 이메일 서버를 구축하여 성능 테스트를 진행했습니다. 

이 서버는 실제 Gmail SMTP 서버의 조건을 최대한 유사하게 구현하면서도 테스트 환경의 안정성과 통제 가능성을 높이는 데 초점을 맞췄습니다. 

 

1-1.  Mock 이메일 서버 설계 원칙

Mock 서버는 SMTP 대신 HTTP 기반의 REST API를 통해 이메일 발송 요청을 처리하도록 설계되었습니다. 

Gmail SMTP 서버와 비슷한 환경을 재현하기 위해 아래 두 가지 조건을 적용했습니다. 

 

1. Rate Limit

이메일 발송 계정별로 초당 최대 5개의 요청만 처리하도록 제한했습니다.

 

2. 응답 시간

Gmail SMTP 평균 발송 응답 시간(4,000ms)을 기준으로, Mock 서버도 동일한 응답 시간을 유지하도록 설정했습니다.

 

1-2.  Mock 이메일 서버 구현 - Rate Limit

Mock 이메일 서버에 Rate Limit 기능을 구현하기 위해 자바와 스프링 환경에서 잘 호환되는 라이브러리를 검토했습니다. 주요 후보로는 Guava, Bucket4j, 그리고 Resilience4j를 선정했으며, 이들 모두 Rate Limit 기능을 제공하지만 각각의 특성과 장단점이 달라 면밀히 비교할 필요가 있었습니다.

 

 

1. Guava

- 자바의 유틸리티 라이브러리로, 다양한 기능을 제공하며 Rate Limit 구현을 위한 RateLimiter 클래스도 포함하고 있습니다.

- 간단한 Rate Limit 구현에는 적합하지만, 세밀한 구성이나 확장성이 다소 제한적입니다.

 

2. Resilience4j

- 장애 복구와 회복력을 위한 다양한 기능(서킷 브레이커, 리트라이, Rate Limit 등)을 제공하는 강력한 라이브러리입니다

- 그러나 Rate Limit 외에도 많은 기능이 포함되어 있어, 단순히 요청량 제한만 구현해야 하는 현 상황에서는 과도한 선택이 될 수 있었습니다.

 

3. Bucket4j

- 자바 기반의 전문 Rate Limit 라이브러리로, 토큰 버킷 알고리즘을 사용해 유연하고 정밀한 Rate Limit을 제공합니다.

- 사용량 제한, 버스트 트래픽 허용, 그리고 분산 환경에서도 안정적인 동작을 보장하는 특징이 있습니다.

 

 

Mock 이메일 서버의 Rate Limit 기능 구현을 위해 여러 라이브러리를 비교한 끝에, Bucket4j를 선택했습니다.

Bucket4J를 활용한 RateLimit 기능 구현
Rate Limit 적용 코드

1-3.  Mock 이메일 서버 구현 - 응답 시간

Gmail SMTP 서버의 평균 발송 응답 시간은 약 4,000ms(4초)로, Mock 메일 서버에서 응답 시간을 일정하게 유지하기 위해 Java의 Thread.sleep() 메서드를 활용했습니다. 

응답 시간 4초 유지할 수 있도록 적용

 

1-4.  Mock 이메일 서버 구현 - 연동

부하 테스트할 때 실제로 이 Mock 이메일 서버를 구현하기 위해서는 서버에서 이메일 발송 요청을 할 때 이 Mock 이메일 서버로 요청이 갈 수 있도록 구현해야 합니다.

실제 서버에 테스트를 위한 코드를 구현하기에는 비즈니스 로직에 방해가 될 것 같아 서버와 같은 모양을 가진 모의 서버를 하나 만들어 테스트할 예정입니다.

 

실제 모의 서버의 MailService를 확인하면

이메일 발송 요청 시 Mock 메일 서버로 요청
Mock 메일 서버 요청 API

2. 비동기 방식의 성능 테스트

비동기 방식은 동기 방식과 달리 요청에 대한 응답 시간이 곧 처리 완료 시간을 의미하지는 않습니다. 

응답이 반환된 이후에도 백그라운드에서 작업이 계속 진행되기 때문입니다. 

이로 인해 JMeter와 같은 부하 테스트 도구로는 비동기 로직의 성능을 정확히 측정하기에는 한계가 있었습니다. 

 

그렇기 때문에 비동기로 호출되는 이메일 발송 기능에 대해 요청시간과 처리 완료 시간을 측정할 필요가 있었습니다. 

다른 성능 지표들의 계산을 쉽게 하도록 하기 위해 DB에 저장하도록 했습니다. 

 

요청 시간, 처리 완료 시간 DB 기록

 

2-1. TPS 성능 지표 구하기 

TPS는 little's law에 따르면 Active User 수 / 평균응답시간 입니다. 비동기 처리에 적용하면 이는 Active User 수 / 평균 처리시간(초)로 적용할 수 있습니다.

평균 처리 시간은 위 DB에서 저장했던 (처리완료 시간 - 요청 시간) 의 평균을 통해 계산할 수 있습니다. 

 

2-2. 부하 발생기 API

VUsers(동시 요청자 수), interval(요청 간격), loop(반복수) 만큼 부하를 줄 수 있게 사용자가 조절할 수 있습니다. 

 

2-3. 비동기 처리 완료 시간 저장

비동기 처리 완료 시간을 저장하기 위해서는 MailService에서 메일을 보내고 나면 발송 성공 처리했을 때 DB에 저장이 될 수 있도록 했습니다. 

 

3. 비동기 방식의 성능 테스트를 통한 스레드 풀 최적화 

이메일 발송 기능은 @Async를 사용하여 비동기로 처리가 되고 있는데, 비동기 작업을 위해 ThreadPoolTaskExecutor 를 활용해 스레드 풀을 사용하고 있습니다. 성능 테스트를 통해 최적의 Thread pool 의 값을 찾아보도록 하겠습니다. 

 

3-1. 성능 테스트 환경

성능 테스트를 위해 위에서 구현한 Mock 이메일 서버, 가짜 서버, 부하를 줄 수 있는 서버를 사용하며, 다른 지표들도 확인하기 위해 가짜 서버에 프로메테우스와 그라파냐를 활용했습니다. 

 

3-2. 성능 테스트를 통한 적절한 VUsers 값 찾기 

본격적인 비동기 스레드 풀의 값을 찾기 전에 스레드 풀과 VUsers의 값을 어떻게 설정하는게 좋은지 테스트를 진행해보도록 하겠습니다. 

 

테스트 결과 스레드 풀 갯수와 VUsers의 상대적인 차이에 따라 3가지 형태의 그래프가 나타났습니다.

 

스레드 풀 갯수는 14로 고정로 고정한 상태에서 vUser 수를 변화시키며 성능을 분석해보겠습니다. 

 

1. 스레드풀 갯수 > vUsers

들어오는 요청 수 보다 처리할 수 있는 양이 더 많음. vUsers 를 증가시킬 수록 TPS 또한 증가

2. 스레드풀 갯수 = vUsers

들어오는 요청 수 만큼 처리할 수 있음. 최적의 TPS

 

3. 스레드풀 갯수 < vUsers

들어오는 요청 수가 처리할 수 있는 양보다 더 많음. 요청이 큐에 쌓이고, 시간이 지날 수록 TPS 감소

 

 

최적의 TPS를 위해 스레드 풀 갯수와 = VUsers를 동일하게 설정하도록 하겠습니다. 

 

3-3. 성능 테스트를 통한 적절한 비동기 스레드풀 갯수 찾기

위 결과처럼 스레드 풀과 VUsers를 동일하게 설정하는 것이 최적의 TPS를 달성할 수 있는 조건임을 확인하였습니다. 이에 따라 이번 테스트에서는 vUser 수와 스레드 풀 크기를 동일하게 유지한 상태에서, 스레드 풀 크기를 점차 증가시키며 평균TPS가 어떻게 변화하는지 측정하였습니다. 

VUser 수(=스레드 풀) 평균 TPS
5 1.9
8 3.5
10 4.0
14 6.002 (최대)
20 4.7
30 4.471

 

테스트 결과, 스레드 풀 크기 14일 때 평균 TPS가 6.002로 가장 높게 나타났으며, 그 이상으로 스레드 풀과 vUser 수를 늘릴 경우 오히려 TPS가 하락하는 경향을 보였습니다. 이는 ThreadPool의 과도한 증가로 인한 자원 경쟁 또는 오버헤드가 TPS 저하로 이어졌을 가능성을 시사합니다. 

따라서 스레드 풀과 vUser 수를 14로 설정하는 것이 가장 효율적인 성능 구성임을 확인할 수 있었습니다. 

 

3-4. 적절한 Queue Size 설정하기 

우선 Queue Size를 설정해야 하는 이유를 살펴보겠습니다. 비동기 작업에서 ThreadPoolTaskExecutor는 처리할 수 있는 쓰레드 수를 초과하는 요청이 들어올 경우, 대기열(Queue)에 작업을 저장하게 됩니다. 

만약 Queue Size가 지나치게 작게 설정되면, 처리되지 못한 작업이 거부되거나 예외가 발생할 수 있으며, 반대로 너무 크게 설정하면 메모리 사용량이 급증하고, 처리 지연이 누적되어 TPS가 감소할 수 있습니다.  

 

이에 따라 Queue Size를 변화시키며 TPS와 처리 안정성에 미치는 영향을 실험하였습니다. 

스레드 풀과 vUser 수는 모두 14로 고정한 상태에서, Queue Size만 조정해 다음과 같은 결과를 얻을 수 있었습니다. 

Queue Size 평균 TPS 비고
0 5.21 큐가 없어 처리 실패율 증가
10 5.83 약간의 버퍼 확보, TPS 향상
20 5.97 대부분의 요청이 안정적으로 처리됨
30 6.00 최고 TPS, 지연 없이 처리 가능
50 5.85 큐가 커져 TPS 약간 하락
100 5.42 지연 증가로 TPS 하락, 메모리 사용 증가 가능성

 

테스트 결과, Queue Size가 30일 때 평균 TPS가 6.00으로 가장 높았으며, 처리 실패도 발생하지 않고 큐 대기 시간도 안정적으로 유지되었습니다. 따라서 Queue Size는 30으로 설정하도록 하겠습니다. 

최종 비동기 설정 코드