개발 일기

[Spring Boot] 비동기 처리: ApplicationEventPublisher와 @Async를 활용한 성능 개선 본문

Back-End/Spring

[Spring Boot] 비동기 처리: ApplicationEventPublisher와 @Async를 활용한 성능 개선

개발 일기장 주인 2025. 3. 17. 12:45

드링클리 프로젝트에서 멤버 회원가입 로직을 구현하는 과정에서 두 가지 주요 작업을 처리해야 했습니다. 첫 번째는 DB에 유저 정보를 저장하는 작업, 두 번째는 팀 Slack 채널에 회원가입 요청이 들어왔고 처리되었다는 정보를 전달하는 작업입니다. 처음에는 두 작업 모두 동기 방식으로 처리했지만, Slack 처리 부분은 반드시 동기적으로 처리할 필요가 없다고 판단하게 되었습니다. 이를 통해 성능을 개선할 수 있을 것이라 생각하여, Slack 처리 부분을 비동기 처리 방식으로 변경하기로 했습니다. 이 과정에서 ApplicationEventPublisher와 같은 비동기 처리 기법을 활용해 두 방식의 성능 차이를 비교해보기로 했습니다.


 

1.  단순 동기 처리 방식

위 코드에서처럼 처음에는 회원가입 로직을 동기 방식으로 처리했습니다. 이 방식은 순차적으로 작업이 처리되며, 각 단계가 완료될 때까지 다음 단계로 넘어가지 않습니다. 예를 들어, 회원가입 요청이 들어오면 다음과 같은 순서대로 작업이 처리됩니다:

  1. 레디스에서 결과 조회: 회원가입 요청에 포함된 memberId를 기준으로 레디스에서 회원 관련 데이터를 조회합니다.
  2. 멤버 정보 저장: 조회한 정보를 바탕으로 memberSignUpRequest를 memberCommandService를 통해 DB에 저장합니다.
  3. OAuth 등록 상태 변경: 회원의 OAuth 등록 상태를 업데이트합니다.
  4. Slack 메시지 전송: 새로 가입한 회원에 대한 정보를 Slack 채널에 전송하여 팀에 알립니다.
  5. JWT 생성 및 반환: 회원가입 처리가 모두 완료된 후, 생성된 JWT 토큰을 반환합니다.

이처럼 각 작업이 동기적으로 처리되며, 모든 작업이 순차적으로 완료될 때까지 다음 작업이 시작되지 않습니다. 이 방식은 간단하고 직관적이지만, Slack 메시지 전송과 같은 외부 시스템과의 통신이 필요할 때, 그 처리 시간이 회원가입 전체 흐름에 영향을 미칠 수 있다는 단점이 있습니다.

예를 들어, Slack 채널에 메시지를 보내는 데 시간이 걸리면 그만큼 회원가입 요청을 처리하는 시간도 길어지게 됩니다. 이는 사용자가 기다려야 하는 시간을 증가시키고, 시스템의 성능에 영향을 미칠 수 있습니다.

이러한 이유로, Slack 메시지 전송과 같은 외부 시스템과의 통신을 비동기 처리로 바꾸면 성능을 개선할 수 있다는 생각이 들었습니다.

 

AOP를 사용하여 요청 처리 시간을 출력하는 로직을 추가하고, 실제로 시간을 측정해봤습니다.

아래 찍힌 출력을 보면 895ms이다.


2. ApplicationEvent를 사용하여 비동기 처리

이번에는 직접 Slack 웹훅 서비스를 호출하는 방식 대신, ApplicationEvent를 사용하여 비동기 처리 방식을 구현해보았습니다. 이전에는 동기적으로 SlackWebhookService를 직접 호출했지만, 이번에는 ApplicationEvent를 발행하고, 이를 리스너에서 처리하는 방식으로 구현했습니다.

 

2-1. MemberSignUpEvent

먼저, MemberSignUpEvent라는 이벤트 클래스를 만들어 회원가입이 완료되었을 때 이를 발행하도록 했습니다.

이렇게 이벤트를 발행함으로써, SlackWebhookService와 같은 외부 서비스와의 의존성을 최소화하여 두 서비스 간에 결합도를 줄여줍니다.

 

2-2. MemberSignUpListener 리스너 구현

이제 MemberSignUpEvent가 발행되면 이를 받아 처리하는 MemberSignUpListener를 구현합니다.

이 리스너는 ApplicationListener<MemberSignUpEvent>를 구현하여, 이벤트를 수신하고 비동기적으로 처리할 수 있게 합니다.

리스너는 @Async 어노테이션을 사용하여, Slack 알림 메시지를 비동기적으로 처리합니다.

 

Spring 4.2 이전
이벤트 객체는 반드시 ApplicationEvent를 상속해야 했음. 이벤트 리스너는 반드시 ApplicationListener<T>를 구현해야 했음.

Spring 4.2 이후
모든 Object 타입을 이벤트 객체로 사용할 수 있음 (ApplicationEvent 상속 불필요). @EventListener를 사용하면 ApplicationListener<T>를 구현하지 않아도 이벤트 리스너를 만들 수 있음.

 

따라서 아래와 같이 수정할 수 있었다.

이전에는 Event 객체가 ApplicationEvent를 반드시 상속해야 했기 때문에 class 타입으로만 정의할 수 있었습니다.

하지만 Event 객체는 본래 불변으로 다뤄야 하므로, 이제는 record를 활용하여 더욱 간결하게 불변 객체를 생성할 수 있게 되었습니다.

MemberSignUpListener에서는 기존의 ApplicationListener<T> 대신 @EventListener를 사용하여 이벤트를 처리했습니다. 

하지만 @EventListener 대신 @TransactionalEventListener를 선택한 이유는 회원 가입 완료 알림을 트랜잭션이 성공적으로 커밋된 이후에만 보내기 위함입니다.
즉, 트랜잭션이 실패한 경우에는 알림을 보내지 않으려는 의도에서 AFTER_COMMIT 속성을 사용했습니다. 

 

이외에도 아래와 같은 속성 값이 있어 트랜잭션 처리 결과에 따라 다른 설정이 가능합니다.

AFTER_COMMIT (기본값) 트랜잭션 커밋 후 트랜잭션이 성공적으로 완료된 경우 실행
AFTER_ROLLBACK 트랜잭션 롤백 후 트랜잭션이 실패하여 롤백된 경우 실행
AFTER_COMPLETION 트랜잭션 완료 후 커밋 또는 롤백 여부와 관계없이 실행
BEFORE_COMMIT 트랜잭션 커밋 직전 커밋되기 직전에 실행

 

이 방식 또한 @MeasureExceptionTime으로 처리 시간을 계산해봤습니다.

위와 같이 82ms이 찍혔습니다.


3. 성능 비교: 동기 처리 방식 vs 비동기 처리 방식

동기 처리 방식과 비동기 처리 방식을 비교한 결과, 두 방식 간의 처리 시간 차이가 매우 큰 것으로 나타났습니다. 구체적으로, 동기 처리 방식에서는 전체 요청 처리 시간이 895ms였던 반면, 비동기 처리 방식에서는 82ms로 약 10배 이상의 시간이 단축되었습니다.

이 정도 차이가 나는 것을 보면, 비동기 방식이 성능 면에서 매우 유리하다는 것을 알 수 있습니다. 물론, 각 요청에 따라 차이는 있을 수 있지만, 10배 이상의 성능 차이는 충분히 의미 있는 개선이라 할 수 있습니다.

 

비동기 처리를 도입함으로써, Slack 웹훅 작업이 완료될 때까지 기다릴 필요가 없어졌습니다. 그 결과, 요청 처리 시간이 먼저 출력되고, Slack 웹훅 처리 결과가 뒤에 출력되는 것을 확인할 수 있었습니다. 이러한 비동기 처리 방식 덕분에 메인 요청 처리 흐름을 지연시키지 않고, Slack 알림과 같은 부수적인 작업을 백그라운드에서 처리할 수 있게 되어 성능을 크게 개선할 수 있었습니다.