
프로젝트에서 회원가입 및 회원탈퇴를 할 때 사용자에게 가입한 이메일로 메일을 보내는 로직을 구현하였다. Java mail sender를 활용해서 기존 회원가입, 회원탈퇴에 관련된 컨트롤러에 로직을 추가하였다. 그러나 SMTP 서버와 통신하는 과정에서 어쩔 수 없이 속도가 느려져서 메일 서비스를 사용하지 않았을 때와 사용했을 때의 속도 차이가 체감상 많이 느껴졌다. 메일 서비스의 유무에 대한 속도를 측정해보았다. 테스트코드는 아래와 같이 작성해서 실제 api를 전송해보는 테스트를 진행하였다.
@Test
@DisplayName("회원가입 이메일 전송 시간 테스트")
public void testJoinEmailPerformance() throws Exception {
// given
String requestBody = """
{
"nickname": "testUser"
}
""";
// when
long totalDuration = 0;
long startTime = System.currentTimeMillis();
mockMvc.perform(patch("/members/oauth2/join")
.header("Authorization", "Bearer " + accessToken)
.contentType(MediaType.APPLICATION_JSON)
.content(requestBody))
.andExpect(status().isOk());
long endTime = System.currentTimeMillis();
totalDuration += (endTime - startTime);
// then
System.out.println("Average Join Email Performance Test Duration: " + totalDuration + "ms");
}
oauth 로그인만 사용하였기 때문에 우리 프로젝트에서는 실제 소셜 로그인 이후에 회원가입을 따로 진행한다. 위 테스트는 소셜 로그인 회원가입에 대한 엔드포인트 테스트이다.
먼저, 이메일 전송 로직이 없는 엔드포인트의 응답 시간을 측정해보았다.
Average Join Email Performance Test Duration: 95ms
다음으로 이메일 전송 로직이 있는 엔드포인트의 응답시간이다.
Average Join Email Performance Test Duration: 4797ms
95ms -> 4797ms 로 약 50배 가량 차이가 나는 것을 확인하였다.
이를 확인하고, 나중에 많은 사용자가 회원가입을 하는 상황이 생기면 스레드가 부족하게 되어 작업 처리 지연 및 시스템 과부하가 발생할 수 있는 매우 큰 문제가 발생할 수 있겠다는 생각이 들었다.
비동기로 처리해야겠다는 판단이 가장 먼저 들었다. 비동기 처리란, 요청을 받은 후 작업이 완료될 때까지 스레드를 차단하지 않고 다른 작업을 병렬적으로 진행할 수 있도록 하는 방식이다. 작업이 완료되면 콜백이나 특정 메커니즘을 통해 결과를 알리는 방식으로 동작하며, 이를 통해 효율적인 자원 활용이 가능하며 응답속도가 지연되지 않아 사용자로 하여금 경험이 향상될 수 있다.
@Async
public void sendEmail(String receiver, String nickname, String type) {
try {
MimeMessage message = mailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8");
helper.setTo(receiver);
...
위 코드처럼 이메일을 전송하는 함수에 @Async를 붙이면 해당 함수가 비동기로 처리된다는 것을 의미한다. 그리고 다시 테스트 코드를 돌려보면 아래와 같은 결과를 볼 수 있다.

Average Join Email Performance Test Duration: 88ms
응답 속도는 이메일 로직을 사용하지 않은 것과 같은 속도를 보였다. 그러나 위의 이미지와 같이 응답을 왔지만 아직 테스트가 돌아가고 있는 것을 알 수 있었다. 즉, 사용자에게는 응답을 보냈지만 백그라운드에서는 이메일을 전송하는 로직을 진행하고 있다는 의미이다.
그러나 아직 고민을 마무리 할 수 없었다. 프로젝트에서는 현재 Spring Cloud Config, Cloud Bus 떄문에 RabbitMQ를 사용하고 있기 때문에 이메일을 보내는 로직을 비동기로 처리하되 RabbitMQ를 활용하여 독립성과 확장성을 보장하면 어떨까하는 생각이 들었다. Async를 활용하는 방법보다는 약간의 시간 증가가 있을 수 있지만 나중에 대규모 트래픽 테스트를 할 때 더 유용하지 않을까하는 생각이 들었다.
프로젝트에서 RabbitMQ를 사용하고 있었기 때문에 몇 개의 코드만 추가하면 바로 사용할 수 있었다. 먼저 Queue, Exchange, RoutingKey를 지정하고 리스너를 지정해주면 끝난다. sendEmail 대신 sendEmailRequest 함수를 추가로 생성해서 Queue에 메세지를 보내는 로직을 구현하면 된다. 그렇게 되면 실제로 컨트롤러에 회원가입에 대한 로직이 들어왔을 때 아래와 같은 프로세스가 진행된다.
회원가입 api 요청 -> sendEmailRequest -> Queue에 메세지 전달 -> 200 응답
RabbitMQ 리스너가 메시지를 수신 -> sendEmail -> 이메일 전송
이렇게 두 개의 과정이 독립적으로 실행된다.
추가한 코드를 간단히 정리하면 다음과 같다.
public void sendEmailRequest(String receiver, String nickname, String type) {
EmailRequest emailRequest = new EmailRequest(receiver, nickname, type);
rabbitTemplate.convertAndSend(
rabbitMQProperties.getExchangeName(),
rabbitMQProperties.getRoutingKey(),
emailRequest
);
log.info("Email request sent to RabbitMQ: {}", emailRequest);
}
@Slf4j
@Service
@RequiredArgsConstructor
public class EmailConsumer {
private final EmailService emailService;
private final RabbitMQProperties rabbitMQProperties;
@RabbitListener(queues = "#{rabbitMQProperties.queueName}")
public void consumeEmailRequest(EmailRequest emailRequest) {
log.info("Consuming email request from RabbitMQ: {}", emailRequest);
emailService.sendEmail(
emailRequest.getReceiver(),
emailRequest.getNickname(),
emailRequest.getType()
);
}
}
각각 sendEmailRequest 함수와 소비자 함수에 대한 코드이다. 컨트롤러에는 기존의 sendEmail 대신 sendEmailRequest로 바꾸어주었다. 이제 테스트 코드를 실행해보자.

Average Join Email Performance Test Duration: 89ms
콘솔에는 RabbitMQ에 대한 메세지가 정상적으로 큐에 보내지고 리스너가 이를 받았다는 로그를 확인할 수 있었다. 실제 RabbitMQ를 확인하여도 동일한 결과를 알 수 있다.

앞으로 프로젝트에 쿠버네티스를 도입하고 각종 대규모 트래픽에 대한 성능 부하 테스트를 진행할 예정이기 때문에 두 번째 방법인 RabbitMQ를 선택하였다. 이메일 전송 로직이 매우 간단한 내용이라 약간의 오버엔지니어링이라고 생각이 들긴하였지만 추후 성능 테스트에 빛을 내줄 것이라고 기대한다. 결론적으로 두 가지의 방법 모두 기존의 테스트에서 측정한 응답 시간으로 보았을 때, 굉장한 시간 단축을 할 수 있었다.
약 4700ms에서 약 90ms 로 응답시간이 단축되었으며, 이는 약 98%의 성능 향상이다.