비동기 처리와 실패 재시도 이해하기

배경

sprint mission 11에서는 파일 저장과 알림 생성 같은 작업을 비동기로 처리했다.

비동기는 말 그대로 “요청을 받은 흐름과 별도로 나중에 실행한다”는 뜻이다.

사용자 요청을 처리하는 스레드가 모든 일을 끝낼 때까지 기다리지 않고, 오래 걸리는 작업은 다른 스레드에게 맡긴다.

왜 비동기가 필요할까

파일 업로드를 예로 들면 실제 바이트 저장은 오래 걸릴 수 있다.

특히 S3 같은 외부 저장소를 사용하면 네트워크 상황에 따라 응답 시간이 길어질 수 있다.

동기 처리라면 흐름은 이렇게 된다.

사용자 요청
  -> DB 저장
  -> 파일 저장이 끝날 때까지 대기
  -> 응답 반환

비동기 처리라면 이렇게 바뀐다.

사용자 요청
  -> DB 저장
  -> 이벤트 발행
  -> 응답 반환

별도 스레드
  -> 파일 저장 처리

사용자 입장에서는 응답이 더 빨리 오는 것처럼 느껴진다.

AsyncConfig가 하는 일

비동기를 사용하려면 @EnableAsync 설정이 필요하다.

그리고 실제로 비동기 작업을 실행할 TaskExecutor가 필요하다.

이번 프로젝트에서는 비동기 스레드에서도 다음 정보가 유지되도록 TaskDecorator를 사용했다.

  • MDC의 Request ID
  • SecurityContext의 인증 정보

MDC는 로그에 requestId 같은 값을 남기기 위한 저장 공간이다.

비동기 스레드는 원래 요청 스레드와 다르기 때문에 MDC나 인증 정보가 자동으로 이어지지 않을 수 있다. 그래서 TaskDecorator로 원래 스레드의 정보를 복사해 넘긴다.

비동기의 단점

비동기는 응답 속도를 개선할 수 있지만 단점도 있다.

가장 큰 단점은 실패를 사용자에게 바로 알려주기 어렵다는 점이다.

예를 들어 사용자가 메시지를 보냈고 서버가 바로 응답을 반환했다. 그런데 그 뒤에 비동기로 처리하던 S3 업로드가 실패하면 사용자는 이미 성공 응답을 받은 상태다.

그래서 비동기 작업은 실패했을 때 기록과 후속 처리가 중요하다.

S3 업로드 재시도

S3 파일 저장은 외부 네트워크와 AWS에 의존한다.

잠깐 네트워크가 불안정하거나 일시적으로 실패할 수 있다. 이런 경우 한 번 실패했다고 바로 포기하기보다 몇 번 다시 시도하는 것이 좋다.

이번 프로젝트에서는 Spring Retry를 사용했다.

핵심 어노테이션은 다음과 같다.

  • @Retryable: 실패하면 다시 시도한다.
  • @Recover: 모든 재시도가 실패했을 때 마지막으로 실행된다.

흐름은 다음과 같다.

S3 업로드 시도
  -> 실패
  -> 다시 시도
  -> 또 실패
  -> 다시 시도
  -> 최종 실패
  -> @Recover 실행

실패 정보를 관리자에게 알리기

모든 재시도가 실패하면 관리자에게 알림을 생성했다.

알림에는 나중에 디버깅할 수 있도록 다음 정보를 포함했다.

  • 실패한 작업 이름
  • Request ID
  • BinaryContent ID
  • 예외 메시지

이 정보가 있어야 로그와 DB 데이터를 연결해서 원인을 찾을 수 있다.

예를 들어 Request ID가 있으면 같은 요청에서 남긴 로그를 추적할 수 있다.

이번 작업에서 바뀐 흐름

S3 업로드 실패 흐름은 처음에는 직접 관리자 알림을 저장하는 방식이었다.

이후 Kafka 요구사항까지 진행하면서 최종 구조는 이벤트 발행 방식으로 바뀌었다.

S3 업로드 실패
  -> S3UploadFailedEvent 발행
  -> Kafka로 이벤트 전송
  -> Kafka Listener가 관리자 알림 생성

처음에는 같은 서버 안에서 알림을 만들었지만, 나중에는 알림 서비스를 분리할 수 있도록 Kafka를 거치는 구조로 바뀐 것이다.

테스트와 확인 방법

테스트는 전체 테스트로 확인했다.

./gradlew.bat test

비동기 성능 비교는 파일 저장에 일부러 지연을 넣고 메시지 생성 API 시간을 비교하는 방식으로 확인할 수 있다.

Actuator 설정을 통해 다음 지표를 확인할 수 있다.

/actuator/metrics/message.create.async

배운 점

비동기는 단순히 빠르게 만들기 위한 기술이 아니다.

비동기로 처리하면 응답 흐름과 실제 처리 흐름이 분리된다. 그래서 실패 처리가 더 중요해진다.

동기 코드에서는 실패하면 바로 에러 응답을 줄 수 있다. 하지만 비동기 코드에서는 사용자가 이미 응답을 받은 뒤 실패할 수 있다.

그래서 비동기 작업에는 다음이 함께 필요하다.

  • 상태 값
  • 재시도
  • 실패 기록
  • 관리자 알림
  • 추적 가능한 Request ID

이번 작업을 통해 비동기는 성능 개선뿐 아니라 운영 관점의 실패 대응까지 같이 설계해야 한다는 것을 배웠다.