비동기 처리와 실패 재시도 이해하기
비동기 처리와 실패 재시도 이해하기
배경
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
이번 작업을 통해 비동기는 성능 개선뿐 아니라 운영 관점의 실패 대응까지 같이 설계해야 한다는 것을 배웠다.