ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring Event와 @Aysnc를 사용해 회원가입 개선
    Project Trouble Shooting/[EATceed] 몸무게 증량 어플 2024. 6. 9. 12:38
    728x90

     

    이번 포스팅에서는 회원가입 코드를 Spring Event와 @Async를 이용해 개선해보려고 합니다.

    문제점

    현재 서비스의 회원가입 기능은 다음과 같은 과정으로 이루어져 있습니다.

    1. 회원이 가입할 수 있는 유저인지 확인
    2. 특정 이메일로 인증 메일 발송
    3. 회원 정보 생성

    이처럼 하나의 흐름 안에 "메일 보내기"와 "회원가입"이 강하게 결합되어 있으면 다음과 같은 문제가 발생합니다.

    • 의존성이 높아져 재사용성이 떨어짐
    • 단위 테스트가 어려움

    Spring Event 도입

    이메일 전송 기능은 여러 도메인에서 활용될 가능성이 높기 때문에 회원가입 서비스에서 직접 호출하는 방식보다는 Event를 활용하는 것이 좋다고 생각했습니다.

     

    비동기 도입

    비동기 처리 이전에는 배포 직후 요청에서 Request Timeout이 발생하는 문제가 있었습니다.

    ALB(Application Load Balancer)의 idle timeout이 50초로 설정되어 있음에도 불구하고, 회원가입 API 요청이 50초 이상 소요된 것을 확인할 수 있었습니다. 이에 따라 애플리케이션의 처리 속도를 개선할 방법이 필요했습니다. 따라서, @Async를 도입하기로 하였습니다.



    (아래는 Request Time Out 문제를 해결하기 위해 고민했던 포스팅입니다.)
     

    Request Time Out 문제 해결

    프로젝트를 진행하며 배포 직후 회원 가입 요청에서 Request Time Out 응답이 발생하였습니다.  문제 상황 회원가입 API 요청 시 Request Time Out 문제가 발생하였습니다. 해당 API에서 수행하는 작업은

    rasony.tistory.com

     

     
     
    비동기를 적용하면서 아래와 같은 점을 고민하였습니다.
     

     

    @Async를 handle 메서드에서 사용 vs sendEmail 메서드에서 사용

     

    @Async를 사용하면 새로운 스레드가 만들어져 작업을 처리합니다.

    이때, handle 메서드에서 @Async를 사용하면, 하나의 스레드가 암호화 작업 및 이메일 보내는 작업을 수행합니다. 

    반면, sendEmail 메서드에 @Async를 사용하게 되면, 기존 스레드가 암호화 작업을 수행하고 새로운 스레드가 이메일 보내는 작업을 수행합니다. 이렇게 되면, 기존 스레드가 가진 정보를 새로운 스레드에 제공해줘야하기 때문에 스레드 컨텍스트 스위칭 비용이 발생하게 됩니다.

    따라서, 추가적인 비용이 발생하지 않는 handle 메서드에 @Async를 사용하였습니다.

     

     

    ThreadPoolExcecutor의 스레드 풀 관련 설정 

     

    Spring에서 @Async 어노테이션을 사용하면 비동기 작업을 활성화할 수 있습니다. 이때 AsyncTaskExecutor 인터페이스의 구현체를 사용하여 비동기 작업을 수행하게 됩니다. 기본적으로 Spring은 SimpleAsyncTaskExecutor를 사용하지만, 아래와 같은 이유로  ThreadPoolTaskExecutor를 활용하는 게 더 좋다고 생각합니다.

     

    • SimpleAsyncTaskExecutor을 사용하면 비동기 작업마다 새로운 스레드를 생성해야합니다. 스레드를 생성시키고, 스레드를 종료 시키는 것은 많은 비용이드는 일입니다.
    • 동시에 많은 요청이 들어온다고 할 때 스레드 수를 제어할 수 없습니다. 무한정 증가할 수도 있어서, OOM 문제를 발생시킬 수도 있습니다.

    저는 아래와 같이 스레드 풀 관련 설정을 하였습니다.



    corePoolSize는 스레드 풀의 기본 크기를 의미합니다. 스레드 풀을 실행할 때 초기에 corePoolSize만큼의 스레드를 생성하려면, executor.setPrestartAllCoreThreads(true)로 설정해야 합니다.

    또한, 요청이 증가하여 스레드 수가 corePoolSize에 도달하면, 이후 작업은 queueCapacity 크기만큼 작업 큐에 저장됩니다.

    만약 작업 큐가 가득 차면, maxPoolSize만큼 추가로 스레드가 생성되어 처리할 수 있습니다.

    현재 EATceed 서비스의 회원 수는 50명 미만이며, 실제 활성 사용자 수는 이보다 더 적은 것으로 판단됩니다.

    이를 고려하여, 개발 서버에서 30명 동시 요청을 보내 성능 테스트를 진행했습니다.

     

     

    개발 서버에서 사용하는 제 메일은 메일 인증이 완료되지 않은 상태이므로, 30개의 인증 메일이 수신되어야 합니다.
    테스트 결과, 모든 메일이 정상적으로 도착하는 것을 확인할 수 있었습니다.


    또한, 개발 서버는 Spring Boot, MariaDB, Redis를 Docker Compose로 실행하고 있어 운영 서버보다 스펙이 낮습니다.
    따라서, 운영 서버는 이보다 높은 트래픽을 충분히 감당할 수 있을 것으로 예상됩니다.

     

     

     

    비동기 처리 시 예외 핸들링

    Async 메서드에서 발생하는 예외는 호출자에게 전파되지 않습니다. 예외를 적절히 처리하려면 AsyncUncaughtExceptionHandler를 활용하여 예외를 처리해야 합니다.

     

     

    따라서, AsyncConfig 클래스에 getAsyncUncaughtExceptionHandler() 메서드를 구현해줍니다. 그리고 해당 메서드에서 비동기 예외를 처리할 ExceptionHandler를 만들어서 예외를 알맞게 처리하면 됩니다.

     

    비동기 처리 결과

     

    비동기로 처리한 결과 아래와 같이 수행 시간이 효과적으로 개선되었습니다. (비동기 처리 후 해당 API의 속도는 약 12배 증가하였습니다.)

     

    비동기 처리 전

     

     

     

    비동기 처리 후

     

     

     

     

    부족한 포스팅을 읽어주셔서 감사합니다!

     

     

     

    728x90
Designed by Tistory.