⎮ Status Code 기반 예외처리
이전 포스팅에서 예외처리 필요성을 충분히 설명했으니 바로 본론으로 넘어가겠습니다
Status Code 기반 예외처리는 다음과 같습니다
먼저 서버에 네트워크 요청을 보내면, 상태 코드 응답을 함께 받게 됩니다
이때 이 응답은 200 ~ 599 정도의 숫자로 나타납니다
보통 200~299를 성공 케이스로 봅니다(물론 서버 개발자의 성향에 따라 다를 수 있습니다)
import Alamofire
AF.request("https://api.example.com/user/info")
.validate(statusCode: 200..<300) // 성공 범위 지정 - 명시적으로 작성
.responseJSON { response in
switch response.result {
case .success(let value):
print("성공:", value)
case .failure(let error):
if let statusCode = response.response?.statusCode {
switch statusCode {
case 400:
print("잘못된 요청 (Bad Request)")
case 401:
print("인증 실패 (Unauthorized)")
case 403:
print("접근 권한 없음 (Forbidden)")
case 404:
print("리소스를 찾을 수 없음 (Not Found)")
case 500...599:
print("서버 에러 (Server Error)")
default:
print("기타 에러: \(statusCode)")
}
} else {
print("네트워크 오류 또는 응답 없음: \(error.localizedDescription)")
}
}
}
validate(statusCode:)는 지정한 범위(200~299)가 아니면 자동으로 실패(failure)로 분기됩니다
failure 코드 블록에서는 statusCode를 확인해 세부 에러 유형을 분류할 수 있습니다
또 이러한 분기는 enum Type의 커스텀 에러를 사용하여 깔끔하게 처리할 수 있습니다
// Custom NetworkError 정의
enum NetworkError: Error {
case badRequest // 400
case unauthorized // 401
case forbidden // 403
case notFound // 404
case serverError // 500~599
case noResponse // 네트워크 응답 없음
case decodingError // JSON 디코딩 실패
case unknown(statusCode: Int?) // 그 외
var localizedDescription: String {
switch self {
case .badRequest:
return "잘못된 요청입니다."
case .unauthorized:
return "인증에 실패했습니다."
case .forbidden:
return "접근 권한이 없습니다."
case .notFound:
return "요청한 리소스를 찾을 수 없습니다."
case .serverError:
return "서버에서 오류가 발생했습니다."
case .noResponse:
return "서버 응답이 없습니다."
case .decodingError:
return "데이터 파싱에 실패했습니다."
case .unknown(let code):
return "알 수 없는 오류가 발생했습니다. (\(code.map(String.init) ?? "no code"))"
}
}
}
import Alamofire
func requestUserInfo(completion: @escaping (Result<User, NetworkError>) -> Void) {
AF.request("https://api.example.com/user/info")
.validate() // 기본적으로 200..<300 성공으로 처리
.responseDecodable(of: User.self) { response in
// 상태 코드 접근
guard let statusCode = response.response?.statusCode else {
completion(.failure(.noResponse))
return
}
switch response.result {
case .success(let user):
completion(.success(user))
case .failure:
let error: NetworkError
switch statusCode {
case 400:
error = .badRequest
case 401:
error = .unauthorized
case 403:
error = .forbidden
case 404:
error = .notFound
case 500...599:
error = .serverError
default:
error = .unknown(statusCode: statusCode)
}
completion(.failure(error))
}
}
}
⎮ 서버 메시지 기반 예외처리
위와는 다르게 서버에서 에러 응답을 넘겨주는 케이스도 있습니다
{
"code": "E001",
"message": "잘못된 요청 형식입니다."
}
이 같은 경우에 message 값을 가져오려면 디코딩 과정이 필요하고,
디코딩을 위해 서버의 에러 응답 구조를 모델로 정의해줘야 합니다
struct ServerErrorResponse: Decodable {
let code: String
let message: String?
}
그리고 실패 케이스에서 JSONDecoder를 사용해서 에러 응답을 디코딩해주면 됩니다
AF.request("https://api.example.com/user/info")
.responseDecodable(of: User.self) { response in
switch response.result {
case .success(let user):
print("성공:", user)
case .failure:
if let data = response.data,
let serverError = try? JSONDecoder().decode(ServerErrorResponse.self, from: data) {
print("서버 에러 코드:", serverError.code)
print("서버 에러 메시지:", serverError.message ?? "없음")
} else {
print("서버 에러 바디 디코딩 실패")
}
}
}
이렇게 하면 서버로부터 내려온 에러를 사용자에게 바로 보여줄 수도,
또는 재 가공해서 사용할 수도 있는 것이죠
⎮ NetworkError에 서버 응답 바디 통합하기
위의 구조는 NetworkError와 ServerErrorResponse가 분리되어 있습니다
서버 메시지를 NetworkError 내부로 통합하면 예외 처리는 더 깔끔하게 할 수 있습니다
enum NetworkError: Error {
case serverMessage(ServerErrorResponse)
case badRequest
case unauthorized
case forbidden
case notFound
case serverError
case noResponse
case decodingError
case unknown(statusCode: Int?)
}
extension NetworkError {
var message: String {
switch self {
case .serverMessage(let error): return error.message ?? "서버 오류가 발생했습니다"
default: return localizedDescription
}
}
}
이렇게 Enum Type의 연관값으로 데이터를 주입받으면,
Alamofire 응답 처리 시에
if let data = response.data,
let serverError = try? JSONDecoder().decode(ServerErrorResponse.self, from: data) {
completion(.failure(.serverMessage(serverError)))
} else {
completion(.failure(.unknown(statusCode: statusCode)))
}
다음과 같은 형태로, 에러 바디 기반의 예외처리를 한 곳으로 통합할 수 있습니다
⎮ 서버 응답이 String 또는 Int로 온다면?
서버에서 에러 응답을 "code": "E401" 이런 형태가 아니라,
"code" : 401 로 내려줄 때가 있습니다
이런 경우 ServerErrorResponse가 Decodable에 실패하게 되는데요
이때는 다음과 같이 init 시점에 파싱하면서 사용하는 것이 안전할 수 있습니다
struct ServerErrorResponse: Decodable {
let code: String
let message: String?
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let stringCode = try? container.decode(String.self, forKey: .code) {
self.code = stringCode
} else if let intCode = try? container.decode(Int.self, forKey: .code) {
self.code = String(intCode)
} else {
self.code = "UNKNOWN"
}
self.message = try? container.decode(String.self, forKey: .message)
}
private enum CodingKeys: String, CodingKey {
case code, message
}
}
⎮ 결론
예외처리를 체계적으로 구성하면, 단순한 네트워크 오류를 넘어
인증 만료나 서버 점검, 요청 유효성 실패 등 다양한 상황을 안전하게 다룰 수 있습니다.
이어질 다음 포스팅에서는 네트워크 재요청 등의 로직을 작성하는 방법에 대해 다뤄보겠습니다.
'Swift > TOPIC' 카테고리의 다른 글
| Swift | final 키워드에 대한 고찰(feat. Method Dispatch) (0) | 2025.11.12 |
|---|---|
| Json Web Token(JWT)와 Apple Login (0) | 2025.10.24 |
| Swift | 네트워크 예외처리를 해보자(1) (0) | 2025.09.16 |
| Hash / Hashable / Hasher / HashTable (0) | 2025.09.09 |
| Swift | 메모리 누수(2) (Memory Graph) (0) | 2025.09.01 |