[iOS] Moya를 이용한 네트워크 계층 추상화

parkgyurim·2022년 6월 26일
3

iOS

목록 보기
4/5
post-thumbnail

💻 이 글은 Swift 5, Xcode 13.4.1, Moya 15.0.0 를 바탕으로 작성하였습니다.


🤔 Moya가 모야 ㅋㅋ

🔗 Moya - Github

https://github.com/Moya/Moya

등장 배경

대부분 iOS 개발자분들은 Alamofire 를 사용하고 계시거나, 혹시 사용한 적이 없더라도 알고는 있을거에요. AlamofireURLSession 을 추상화하여 보기 쉬운 형태로 네트워킹을 제공하는데요,

오늘 알아볼 MoyaAlamofire 를 한번 더 추상화해서,
네트워크 계층을 템플릿화하여 재사용성을 높이고 가독성을 높여 개발자는 오로지 ResponseRequest 에만 신경을 쓸 수 있도록 하여 생산성을 높일 수 있습니다!

📚 So the basic idea of Moya is that we want some network abstraction layer that sufficiently encapsulates actually calling Alamofire directly. - Moya Github

장점

Moya Github 에서 설명하듯,

  • Compile-time checking for correct API endpoint accesses.
    컴파일시 API 엔드 포인트가 올바른지 체크
  • Lets you define a clear usage of different endpoints with associated enum values.
    Enum을 이용해서 언제, 어디에 사용될지 안전하게 (type-safe) 정의
  • Treats test stubs as first-class citizens so unit testing is super-easy.
    유닛 테스트를 용이하게 만듦.

AS-IS, TO-BE

기존 구조의 문제점으로 크게 3가지를 제시하고 있는데,

  • Makes it hard to write new apps (“where do I begin?”)
    어디서 부터 시작할지 시작의 어려움
  • Makes it hard to maintain existing apps (“oh my god, this mess…”)
    유지보수의 어려움
  • Makes it hard to write unit tests (“how do I do this again?”)
    유닛 테스트 작성의 어려움

Moya 를 사용해서 깔끔한 네트워크 계층 구조를 갖게 하고, Moya 계층을 템플릿화하여 재사용할 수 있게 합니다.


🛠 Install Moya

CocoaPods 를 이용한 Moya 설치

프로젝트 폴더에서 Pod init,
Podfile 에 아래와 같이 필요한 라이브러리 추가 → Pod install

pod 'Moya'
pod 'Moya/RxSwift'
pod 'Moya/ReactiveSwift'
pod 'Moya/Combine'

Swift Package Manager 를 이용한 설치

Xcode 에서 FileAdd Packages
https://github.com/Moya/Moya.git 입력 후 설치

마찬가지로 필요한 라이브러리를 ✅ 체크 해서 선택할 수 있습니다!

이외에 Accio, Carthage 를 이용한 설치는 Moya Github - Installation 을 참고해주세용


🧑‍💻 Let's code via Moya!

API 목록 작성 with Enum

enum UserAPI {
    case LogIn(oAuthProvider : OAuthProvider, accessToken : String)
    case refreshToken
}

Enum 을 사용해서 사용할 API 목록을 작성해줍니다.

Enum 을 사용함으로써 보다 안전하게 사용할 수 있고,
유지보수의 관점볼 때 새로운 API를 추가할때 편리하겠죠?

TargetType 작성

방금 enum으로 작성한 API 목록을 extension 할건데, TargetType 프로토콜을 Conform 하여 API 별로 리퀘스트에 필요한 것들을 지정할 수 있습니다.!

📌 Property of TargetType

  • baseURL : Server base URL 지정
  • path : API Path 지정
  • method : HTTP Method 지정
  • sampleData : Mock Data for Test
  • task : Parameters for request 지정
  • validationType : 허용할 response 정의 - validationType 참고
    기존 Alamofire 의 .validate() 처럼 리스폰스의 StatusCode 에 따라 성공 유무를 판단
  • headers : HTTP headers 적용
    : 기존의 인터셉터의 Adapter 역할을 할 수 있습니다. (ex. 헤더에 JWT 값 넣기)

Switch 문을 활용해서 간편하게 여러 타입의 API 에 대한 값들을 지정할 수 있고,
필요없는 파라미터는 Wild Card 구문 (_) 을 사용해서 표현하면 됩니다!

잘 이해가 되지 않는다면, 제가 작성한 예시코드를 참고하시면 이해에 도움이 될 것 같습니다.

extension UserAPI : TargetType {
    var baseURL: URL { URL(string: ServiceAPI.baseURL)! }
    
    var path: String {
        switch self {
            case .LogIn(oAuthProvider: let type, accessToken: _) : return "/login/oauth/" + type.rawValue
            case .refreshToken : return "/token"
        }
    }
    
    var method: Moya.Method {
        switch self {
            case .LogIn(oAuthProvider: _, accessToken: _) : return .post
            case .refreshToken : return .get
        }
    }
    
    // var sampleData: Data { ... }
    
    var task: Task {
        switch self {
            case .LogIn(oAuthProvider: _, accessToken: let accessToken) :
                let params : [String: String] = [ "accessToken" : accessToken ]
                return .requestParameters(parameters: params, encoding: URLEncoding.default )
            case .refreshToken : return .requestPlain
        }
    }
    
    var headers: [String : String]? {
        switch self {
            case .refreshToken :
                guard let userInfo = UserService.shared.userInfo else { return [ "Content-type": "application/json" ] }
                return [ "Content-type": "application/json", "X-AUTH-TOKEN" : userInfo.token.tokenType + " " + userInfo.token.accessToken ]
            case .LogIn(oAuthProvider: _, accessToken: _) : return nil // [ "Content-type": "application/json" ]
        }
    }
    
    var validationType: ValidationType { .successCodes }
}

Response Struct 생성

받아온 Response를 저장할 구조체를 선언해줍니다.
Alamofire 를 사용할때와 동일한 방식으로 생성하면 됩니다 (Codable 프로토콜을 준수)

struct OAuthLoginResponse : Codable, Equatable {
    var userId : Int
    var name : String
    var email : String
    var imageUrl : String
    var role : String
    var token : Token
}

Request with Moya Provider

이제 준비는 끝났고, 네트워크 요청을 해봅시다!

Moya Provider 생성

private let provider = MoyaProvider<UserAPI>()

네트워크 요청을 수행할 Moya Provider 인스턴스를 생성해줍니다.
코드에서 보는 것처럼 Moya Provider제네릭 타입으로 TargetType 프로토콜을 준수하는 Enum 을 받고 있습니다.

이말은 즉, API 의 동작 범위에 따라 인스턴스를 구분 (ex. 유저 APIs, 게시물 APIs, ...) 하여 생성할 수 있고, 이 인스턴스들을 이용해 동일 동작을 하는 메소드를 사용할때 재사용할 수 있겠죠?!

네트워크 요청 / 처리 - Moya

provider.request(.LogIn(oAuthProvider: .Kakao, accessToken: accessToken)) { response in
    switch response {
        case .success(let result) :
            guard let data = try? result.map(DataResponse<OAuthLoginResponse>.self) else { return }
            print(data)
        case .failure(let err):
            print(err.localizedDescription)
    }
}

생성한 Provider 를 통해 request 를 요청할 수 있습니다.
provider.request 의 파라미터로 아까 생성한 TargetType 을 전달해주세요!

이때 반환되는 response 는 Result<Response, MoyaError> 형태입니다.

네트워크 요청 / 처리 - CombineMoya

import CombineMoya

...

private var subscription = Set<AnyCancellable>()

...

provider.requestPublisher(.LogIn(oAuthProvider: .Kakao, accessToken: accessToken))
    .sink { completion in
        switch completion {
            case let .failure(error) :
                print("LogIn Fail : " + error.localizedDescription)
            case .finished :
                print("LogIn Finished")
        }
    } receiveValue: { recievedValue in
        guard let responseData = try? recievedValue.map(DataResponse<OAuthLoginResponse>.self) else { return }
        print(responseData)
    }.store(in : &subscription)

마찬가지로 Combine.sink() 을 이용하여 비동기적으로 Response 를 처리할 수 있습니다!

🎯 Advanced Usage

Interceptor

Alamofire 를 사용할때는 request 함수에 파라미터로 인터셉터를 전달할 수 있었는데,
Moya 에서는 어떻게 Interceptor 를 사용할까요?

class Interceptor : RequestInterceptor {
    func adapt(...) { ... }
    func retry(...) { ... }
}

...

private let provider = MoyaProvider<UserAPI>(session : Moya.Session(interceptor: Interceptor()))

Moya Provider 를 생성할때, Moya Sessioninterceptor 를 추가해서 전달해주면 됩니다.


👍 마무리

오늘은 Alamofire 의 네트워크 요청을 추상화하는 Moya를 사용하는 방법에 대해 알아보았습니다.

처음에는 굳이 왜 쓰지? 라는 생각이 들었는데, 사용하면 할수록 enum 을 활용해서 좀 더 안전한 방식으로 캡슐화된 요청을 보낼 수 있고, 코드를 직관적으로 작성할 수 있어 가독성이 높아지는 장점이 있었습니다. 또한 재사용이 용이한 점, API 추가/삭제가 편리한 점도 있었습니다!

틀린 정보 또는 궁금한 점이 있다면 댓글 부탁드립니다! 읽어주셔서 감사합니다‼️

0개의 댓글