본문 바로가기
iOS

[iOS] 재귀 함수를 활용한 토큰 갱신 (2)

by seongminmon 2024. 10. 24.

이전 글

https://k2417000.tistory.com/40

 

문제 상황

프로젝트 진행 중 토큰 갱신을 처리하기 위해 이전에 했던 대로 RequestInterceptor를 사용하려 함

기존 retry 함수에서 statusCode를 통해 통신 실패의 이유가 엑세스 토큰 만료 상황이라고 판단하였으나, 이번엔 통신 실패 시 상태 코드가 400번으로 모두 동일하고, errorCode에 따라 달라져서 사용이 어려웠음

retry 구문내에서 응답값을 받아 decoding하여 처리해보려 했으나, 잘 되지 않아 RequestInterceptor를 사용하지 않고, 재귀 함수를 통해 직접 구현해 보기로 결정함

// 엑세스 토큰 만료 시 서버 응답
{
  "errorCode": "E05"
}

 

네트워크 요청 함수

func request<Router: TargetType, ModelType: Decodable>(api: Router) async throws -> ModelType {
    let data = try await performRequest(api: api)
    return try handleResponse(data: data, api: api)
}

 

외부에서 네트워크를 요청할 때 사용하는 함수

어떤 API를 사용할지와 decoding할 모델 타입을 정해서 호출함

 

private func performRequest<Router: TargetType>(api: Router) async throws -> Data? {
    let request = try api.asURLRequest()
    let response = await AF.request(request).serializingData().response
    return try await handleStatusCode(response: response, api: api)
}

 

실제로 통신하는 함수

응답을 Data? 형태로 return함

토큰 갱신 이후 재호출할 때 이 함수를 호출하게 됨

asURLRequest()에서 UserDefaults 값을 사용해서 Header를 구성하기 때문에 새롭게 갱신된 헤더로 다시 통신을 진행할 수 있음

 

private func handleStatusCode(response: AFDataResponse<Data>, api: any TargetType) async throws -> Data? {
    let statusCode = response.response?.statusCode ?? 0
    
    switch statusCode {
    case 200:
        print("\(api) 성공")
        return response.data
        
    case 400, 500:
        let error = try await handleError(response: response.data, api: api)
        
        // 엑세스 토큰 만료인 경우 기존 요청 재시도
        if let errorResponse = error as? ErrorResponse,
           errorResponse.errorCode == APIError.accessTokenExpired.rawValue {
                   return try await performRequest(api: api)
        }
        throw error
        
    default:
        print("\(api) 알 수 없는 에러")
        throw APIError.etc
    }
}

 

상태 코드로 성공과 실패를 구분하여 처리하는 함수

서버에서 오는 상태 코드는 200, 400, 500번 밖에 오지 않음

엑세스 토큰 만료 시 기존 요청을 재귀적으로 호출함

 

struct ErrorResponse: Decodable, Error {
    let errorCode: String
}

 

private func handleError(response: Data?, api: any TargetType) async throws -> Error {
    do {
        let errorData = try JSONDecoder().decode(
            ErrorResponse.self,
            from: response ?? Data()
        )
        if errorData.errorCode == APIError.accessTokenExpired.rawValue {
            return try await handleTokenRefresh(errorData: errorData)
        }
        return errorData
    } catch {
        return APIError.decoding
    }
}

 

통신 실패 시 응답값을 ErrorResponse Model로 디코딩하여 errorCode를 찾아내는 함수

실패의 이유가 엑세스 토큰 만료라면 엑세스 토큰 갱신 통신을 진행함

 

private func handleTokenRefresh(errorData: ErrorResponse) async throws -> Error {
    do {
        let result: Token = try await request(
            api: AuthRouter.refreshToken(
                refreshToken: UserDefaultsManager.refreshToken
            )
        )
        UserDefaultsManager.refresh(result.accessToken)
        return errorData
    } catch {
        // 엑세스 토큰 갱신 실패 >> 로그인 화면으로 이동
        Notification.changeRoot(.fail)
        return error
    }
}

 

엑세스 토큰 갱신 통신을 진행하고, 결과를 UserDefaults에 저장하는 함수

엑세스 토큰 갱신 실패 시에는 NotificationCenter를 사용해 로그인 화면으로 이동할 수 있도록 함

 

private func handleResponse<T: Decodable>(data: Data?, api: any TargetType) throws -> T {
    guard let data = data else {
        throw APIError.noData
    }
    
    do {
        return try JSONDecoder().decode(T.self, from: data)
    } catch {
        throw APIError.decoding
    }
}

 

data 타입을 modelType으로 decoding하는 함수

 

네트워크 Flow

 

1. 통신 성공 시

 

request(api:) 호출

performRequest(api:) 실행하여 HTTP 요청

서버로부터 200 응답 수신

handleStatusCode()에서 200 확인

handleResponse()를 통해 데이터 디코딩

디코딩된 데이터 반환

 

2. 엑세스 토큰 만료로 통신 실패 시

 

request(api:) 호출

performRequest(api:) 실행하여 HTTP 요청

서버로부터 400/500 응답 수신

handleStatusCode()에서 에러 확인

handleError()에서 액세스 토큰 만료 확인

handleTokenRefresh()를 통해 리프레시 토큰으로 새 액세스 토큰 요청

새 액세스 토큰을 UserDefaults에 저장

원래 요청 재시도

성공 응답 수신 및 데이터 반환

 

3. 리프레시 토큰 만료로 통신 실패 시

 

request(api:) 호출

performRequest(api:) 실행하여 HTTP 요청

서버로부터 400/500 응답 수신

handleStatusCode()에서 에러 확인

handleError()에서 액세스 토큰 만료 확인

handleTokenRefresh() 시도 중 실패

로그인 화면으로 이동하도록 알림 발송