티스토리 뷰
현재 프로젝트에서 Moya 를 사용하고 네트워크 에러 또한 Moya 에서 정의한대로 사용하고 있는데요. 프로젝트를 진행하다 돌아보니 Moya 에서 정의한 에러들을 정확하게 알지 못한다고 느꼈습니다.
그래서 이번에 에러 핸들링 구조를 리팩토링 하기전, 한번 정리를 싹 해보려고 합니다.
고고!
MoyaError
Moya Error 에는 총 9개의 케이스가 있습니다.
public enum MoyaError: Swift.Error {
case imageMapping(Response)
case jsonMapping(Response)
case stringMapping(Response)
case objectMapping(Swift.Error, Response)
case encodableMapping(Swift.Error)
case statusCode(Response)
case underlying(Swift.Error, Response?)
case requestMapping(String)
case parameterEncoding(Swift.Error)
}
사실 케이스 이름과 설명을 보면 뭐하는애인지 대충 알 것 같긴한데,
에러 핸들링을 리팩토링하는 입장에서 대충 알면 안되잖아요? ㅎㅎ.. 그래서 하나씩 코드 레벨로 내려가서 살펴보도록 하겠습니다.
imageMapping
Response의 data를 이미지로 맵핑하는 과정에서 발생할 수 있는 에러
func mapImage() throws -> Image {
guard let image = Image(data: data) else {
throw MoyaError.imageMapping(self)
}
return image
}
이미지 맵핑 에러가 던저지는곳은 Response class 내부에서 reseponse의 data를 이미지로 생성(맵핑) 할때입니다.
참고로 여기서 Image 는 typealias 로 UIImage, NSImage 모두 지원하도록 되어있네요. 큰 라이브러리라서 이런 부분도 잘 케어가 되고 있는 것 같아요👍
#if canImport(UIKit)
import UIKit.UIImage
public typealias ImageType = UIImage
#elseif canImport(AppKit)
import AppKit.NSImage
public typealias ImageType = NSImage
#endif
/// An alias for the SDK's image type.
public typealias Image = ImageType
jsonMapping (Response -> Json)
Response의 data를 json 으로 맵핑하는 과정에서 발생할 수 있는 에러
func mapJSON(failsOnEmptyData: Bool = true) throws -> Any {
do {
return try JSONSerialization.jsonObject(with: data, options: .allowFragments)
} catch {
if data.isEmpty && !failsOnEmptyData {
return NSNull()
}
throw MoyaError.jsonMapping(self)
}
}
마찬가지로 Response 에서 JSONSerialization 을 통해 맵핑을 할떄 실패하면 오류를 뱉네요.
주의할점은 data가 비어있는경우 기본적으로 오류로 취급한다는 점이네요.
그리고 allowFragments 가 JSONSerialization 옵션으로 기본 채택되어 있는데요.
이 옵션은 최상위 객체에 중괄호 {} 가 없어도 파싱을 가능하게 해주는 옵션이라고 합니다!
근데 이 옵션이 Deprecated 되고 fragmentsAllowed 로 바꼈네요.
수정 후 PR을 올려줍시다.
stringMapping
Response의 data를 string 으로 맵핑하는 과정에서 발생할 수 있는 에러
func mapString(atKeyPath keyPath: String? = nil) throws -> String {
if let keyPath = keyPath {
guard let jsonDictionary = try mapJSON() as? NSDictionary,
let string = jsonDictionary.value(forKeyPath: keyPath) as? String else {
throw MoyaError.stringMapping(self)
}
return string
} else {
guard let string = String(data: data, encoding: .utf8) else {
throw MoyaError.stringMapping(self)
}
return string
}
}
특정 오브젝트를 찾아서 string으로 변환할 수 있도록 keyPath 를 지원하네요.
keypath를 못찾거나, data를 String 으로 변환을 못한다면 오류를 던집니다.
objectMapping, jsonMapping
Response의 data를 특정 타입으로 파싱하는 과정에서 발생할 수 있는 에러
func map<D: Decodable>(_ type: D.Type, atKeyPath keyPath: String? = nil, using decoder: JSONDecoder = JSONDecoder(), failsOnEmptyData: Bool = true) throws -> D {
let serializeToData: (Any) throws -> Data? = { (jsonObject) in
guard JSONSerialization.isValidJSONObject(jsonObject) else {
return nil
}
do {
return try JSONSerialization.data(withJSONObject: jsonObject)
} catch {
throw MoyaError.jsonMapping(self)
}
}
let jsonData: Data
keyPathCheck: if let keyPath = keyPath {
guard let jsonObject = (try mapJSON(failsOnEmptyData: failsOnEmptyData) as? NSDictionary)?.value(forKeyPath: keyPath) else {
if failsOnEmptyData {
throw MoyaError.jsonMapping(self)
} else {
jsonData = data
break keyPathCheck
}
}
if let data = try serializeToData(jsonObject) {
jsonData = data
} else {
let wrappedJsonObject = ["value": jsonObject]
let wrappedJsonData: Data
if let data = try serializeToData(wrappedJsonObject) {
wrappedJsonData = data
} else {
throw MoyaError.jsonMapping(self)
}
do {
return try decoder.decode(DecodableWrapper<D>.self, from: wrappedJsonData).value
} catch let error {
throw MoyaError.objectMapping(error, self)
}
}
} else {
jsonData = data
}
do {
if jsonData.isEmpty && !failsOnEmptyData {
if let emptyJSONObjectData = "{}".data(using: .utf8), let emptyDecodableValue = try? decoder.decode(D.self, from: emptyJSONObjectData) {
return emptyDecodableValue
} else if let emptyJSONArrayData = "[{}]".data(using: .utf8), let emptyDecodableValue = try? decoder.decode(D.self, from: emptyJSONArrayData) {
return emptyDecodableValue
}
}
return try decoder.decode(D.self, from: jsonData)
} catch let error {
throw MoyaError.objectMapping(error, self)
}
}
}
코드가 조금 긴데요, 한마디로 response 를 정의해놓은 타입으로 파싱하는 코드입니다. (keyPath 지원)
keyPath 를 지원하는 부분에서 jsonMapping 을 거치고, 디코딩에 실패하면 objectMapping 에러를 내뱉네요.
encodableMapping
URLRequest를 만들때 httpBody 를 인코딩하는 과정에서 발생할 수 있는 에러
mutating func encoded(encodable: Encodable, encoder: JSONEncoder = JSONEncoder()) throws -> URLRequest {
do {
let encodable = AnyEncodable(encodable)
httpBody = try encoder.encode(encodable)
let contentTypeHeaderName = "Content-Type"
if value(forHTTPHeaderField: contentTypeHeaderName) == nil {
setValue("application/json", forHTTPHeaderField: contentTypeHeaderName)
}
return self
} catch {
throw MoyaError.encodableMapping(error)
}
}
Endpoint 를 기반으로 URLRequest 를 만들때 httpBody 에 담길 json을 인코딩 해주는 과정에서 오류가 발생하면 encodableMapping 에러가 발생하네요!
statusCode
Response 의 statusCode가 설정한 statusCode에 포함되지 않는다면 발생하는 에러
func filter<R: RangeExpression>(statusCodes: R) throws -> Response where R.Bound == Int {
guard statusCodes.contains(statusCode) else {
throw MoyaError.statusCode(self)
}
return self
}
⭐️filter 메소드를 실행했을때
Response 의 statusCode가 설정한 statusCode에 포함되지 않는다면, 에러가 발생합니다!
이거 꽤 중요한 부분이라고 생각합니다. 왜냐면 저는 서버통신 -> statusCode 를 자동으로 필터링 해서 statusCode 오류를 던지는줄 알았거든요..!
참고로 이 statusCode는 API를 정의할떄 설정할 수 있습니다.
extension SomeAPI: TargetType {
var validationType: ValidationType {
return .successCodes
}
}
여러 옵션이 있고, custom 해서 사용할수도 있으니 참고하면 좋을듯합니다!
underlying
URLRequest 요청, 수행과정에서 발생할 수 있는 에러
final class func defaultRequestMapping(for endpoint: Endpoint, closure: RequestResultClosure) {
do {
let urlRequest = try endpoint.urlRequest()
closure(.success(urlRequest))
} catch MoyaError.requestMapping(let url) {
closure(.failure(MoyaError.requestMapping(url)))
} catch MoyaError.parameterEncoding(let error) {
closure(.failure(MoyaError.parameterEncoding(error)))
} catch {
closure(.failure(MoyaError.underlying(error, nil)))
}
}
underlying 에러는 Request 요청, 수행중에 발생한 에러입니다.
특히 Alamofire 통신중 발생한 에러는 모두 underlying 에러로 분류됩니다.
/*
Alamofire request 를 보내는 부분에서 colmpletionHandler 를 정의한다.
그리고 핸들러 내부에서 response 를 underlying 에러에 맵핑한다.
*/
func sendAlamofireRequest<T> {
let completionHandler: RequestableCompletion = { response, request, data, error in
let result = convertResponseToResult(response, request: request, data: data, error: error)
// .....
}
}
public func convertResponseToResult(_ response: HTTPURLResponse?, request: URLRequest?, data: Data?, error: Swift.Error?) ->
Result<Moya.Response, MoyaError> {
switch (response, data, error) {
case let (.some(response), data, .none):
let response = Moya.Response(statusCode: response.statusCode, data: data ?? Data(), request: request, response: response)
return .success(response)
case let (.some(response), _, .some(error)):
let response = Moya.Response(statusCode: response.statusCode, data: data ?? Data(), request: request, response: response)
let error = MoyaError.underlying(error, response)
return .failure(error)
case let (_, _, .some(error)):
let error = MoyaError.underlying(error, nil)
return .failure(error)
default:
let error = MoyaError.underlying(NSError(domain: NSURLErrorDomain, code: NSURLErrorUnknown, userInfo: nil), nil)
return .failure(error)
}
}
requestMapping
URLRequest를 만들때 String 을 URL로 변환하는 과정에서 발생할 수 있는 에러
func urlRequest() throws -> URLRequest {
guard let requestURL = Foundation.URL(string: url) else {
throw MoyaError.requestMapping(url)
}
// ....
}
reuquestMapping 은 request 를 만들때 String -> URL 로 변환에 실패하면 발생하는 에러네요!
encodedParamater
URLRequest를 만들때 parameter 를 인코딩하는 과정에서 발생할 수 있는 에러
func encoded(parameters: [String: Any], parameterEncoding: ParameterEncoding) throws -> URLRequest {
do {
return try parameterEncoding.encode(self, with: parameters)
} catch {
throw MoyaError.parameterEncoding(error)
}
}
encodedParameter 는 request 를 만들때 parameter를 인코딩 해줄때 발생하는 에러입니다!
마무리
이렇게 각각의 에러 케이스가 어떤 상황에서 발생하는지 코드레벨로 살펴보았는데요!
Moya 에서 어떻게 에러 핸들링하는지 더 이해하고, 발생하는 상황을 명확하게 알게되어서 얻은게 많습니다ㅎㅎ
영어 원문은 특히 모호하게 기술되어 있어서 어떤 상황에서 발생하는 에러인지 명확하게 알기 어렵더라구요..!
public enum MoyaError: Swift.Error {
/// Response의 data를 이미지로 맵핑하는 과정에서 발생할 수 있는 에러
case imageMapping(Response)
/// Response의 data를 json 으로 맵핑하는 과정에서 발생할 수 있는 에러
case jsonMapping(Response)
/// Response의 data를 string 으로 맵핑하는 과정에서 발생할 수 있는 에러
case stringMapping(Response)
/// Response의 data를 특정 타입으로 파싱하는 과정에서 발생할 수 있는 에러
case objectMapping(Swift.Error, Response)
/// URLRequest를 만들때 httpBody 를 인코딩하는 과정에서 발생할 수 있는 에러
case encodableMapping(Swift.Error)
/// Response 의 statusCode가 설정한 statusCode에 포함되지 않는다면 발생하는 에러
case statusCode(Response)
/// URLRequest 요청, 수행과정에서 발생할 수 있는 에러
case underlying(Swift.Error, Response?)
/// URLRequest를 만들때 String 을 URL로 변환하는 과정에서 발생할 수 있는 에러
case requestMapping(String)
/// URLRequest를 만들때 parameter 를 인코딩하는 과정에서 발생할 수 있는 에러
case parameterEncoding(Swift.Error)
}
이렇게 에러 케이스를 상세하게 알아봤으니, 다음글에서는 이를 바탕으로 에러 핸들링 구조를 리팩토링 해보겠습니다~!
끝~
'iOS-Development' 카테고리의 다른 글
클린 아키텍처 - 아키텍처 적용 (0) | 2023.10.01 |
---|---|
Moya 에러 - 리팩토링 (0) | 2023.09.27 |
Lottie (0) | 2022.05.08 |
CoreData (0) | 2022.04.24 |
UserDefaults (0) | 2022.04.19 |