이전 글
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() 시도 중 실패
로그인 화면으로 이동하도록 알림 발송
'iOS' 카테고리의 다른 글
| [iOS] 이미지 캐싱 (1) - 메모리 캐싱 (NSCache) (0) | 2024.10.03 |
|---|---|
| [iOS] RequestInterceptor를 활용한 토큰 갱신 (1) (0) | 2024.09.30 |
| [iOS] DateFormatter vs Formatted (0) | 2024.09.26 |
| [iOS] 접근 제어자 (0) | 2024.09.07 |
| [iOS] COW(Copy-on-Write) (0) | 2024.08.28 |