연결이 끊기면 Reconnect() 하나가 소켓을 열고 로그인하고 계좌와 화면 데이터를 다시 불러왔다. 실패 로그에는 “재접속 실패”만 남아 어느 단계가 문제인지 알기 어려웠다. 중간 단계가 성공했는데 처음부터 다시 시작해 중복 로그인이나 구독도 생겼다.
재접속은 한 번의 행동이 아니라 서로 다른 실패 조건을 가진 복구 과정이었다.
연결, 인증, 동기화를 나눴다
현재 위치를 상태로 표현했다.
WaitingForNetwork
-> ConnectingTransport
-> RestoringSession
-> SyncingEssentialData
-> Resubscribing
-> Ready
각 단계는 필요한 입력과 성공 결과를 명확히 했다. 네트워크가 없으면 인증을 시도하지 않고, 세션이 만료됐으면 데이터 동기화로 넘어가지 않았다.
단계마다 재시도 정책이 달랐다
전송 연결은 지수 backoff와 jitter를 사용해 제한적으로 재시도할 수 있었다. 인증 만료는 자동 재시도하지 않고 로그인으로 전환했다. 데이터 일부 실패는 핵심 데이터와 부가 데이터에 따라 Ready 진입 여부가 달랐다.
TimeSpan RetryDelay(int attempt)
{
var baseMs = Math.Min(30000, 500 * Math.Pow(2, attempt));
return TimeSpan.FromMilliseconds(baseMs + Random.Shared.Next(0, 300));
}
앱 생명주기와 사용자 취소가 오면 현재 대기를 즉시 끝냈다.
로그는 단계 전이와 이유를 남겼다
단순 성공/실패 대신 이전 상태, 다음 상태, 걸린 시간, 안전한 오류 분류를 기록했다. 토큰과 사용자 데이터는 남기지 않았다.
reconnect transition ConnectingTransport -> RestoringSession elapsed=820ms
reconnect stopped state=RestoringSession reason=credential-expired
운영 로그만으로 어느 계층을 먼저 봐야 하는지 좁힐 수 있었다.
복구 흐름도 제품 기능이었다
정상 연결에서 잘 되는 기능만 만들고 재접속을 예외 처리로 남겨두면 실제 사용에서 상태가 쉽게 꼬였다. 사용자는 이동 중 네트워크를 자주 잃고 앱은 언제든 백그라운드로 갈 수 있었다.
단계를 나눈 뒤 코드는 조금 늘었지만 실패 지점과 다음 행동이 보였다. 이후 긴 비동기 흐름을 만나면 함수 길이보다 의미 있는 체크포인트와 재개 조건을 먼저 찾는다.