티스토리 뷰

iOS-Development

Moya 에러 정리

Gobans 2023. 9. 26. 14:35

현재 프로젝트에서 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
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/08   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31
글 보관함