Moya

Kiwi
12 min readMar 21, 2024

--

많은 사람들이 네트워킹을 시도할때 Moya 라이브러리를 사용한다는 말을 들었다. 하지만 프로젝트를 현재까지 진행하면서 개인적으로 URLSession을 직접 추상화 하여 사용을 하고 있었고, 이미 나름 만족할 정도의 구조가 갖춰져 있었기에 큰 필요성을 느끼지 못하고 있었다.

그러나 프로젝트 리펙토링을 할 시간이 주어짐에 따라 개인적으로 프로젝트의 안정성 및 품질 개선을 고민 하던 와중에 네트워크 통신을 리펙토링 해보자고 마음을 먹었고, 그에 따라 자연스럽게 Moya에 관심을 가지게 되었다.

결론을 미리 말하자면, 많은 사람들이 이미 도입 한 유명한 라이브러리 있으면 객기 부리지 말고 빠르게 도입을 하는게 맞는것 같다.

Why Moya?

Moya github에 나와있는 설명을 그대로 보자면,

기존의 네트워킹 방식은 세가지 문제점이 있다고 한다.

  1. 새 프로젝트를 만들때 어떻게 네트워크 구조를 잡을지 막막하다.

Makes it hard to write new apps (“where do I begin?”)

2. 앱을 유지 및 보수 하기가 어렵다.

Makes it hard to maintain existing apps (“oh my god, this mess…”)

3. 유닛 테스트 하기가 어렵다.

Makes it hard to write unit tests (“how do I do this again?”)

개인적으로 생각하는 위에 세가지 문제점이 발생한 원인은, 기존의 URLSession이라던가 Alamofire같은 경우 추상화를 통한 구조 확립을 하지 않으면, 사용을 하는데 많은 어려움이 따른다.

그렇기 때문에 많은 사람들이 추상화 층을 추가하는 작업을 부가적으로 진행을 하게 되고, 이와 관련해서 많은 경험이나 노하우가 없다면 프로젝트를 시작할때 네트워크 관련해서 많은 어려움이 동반된다.

그리고 이 작업을 하기 위해서는 부가적인 코드들이 많아 질 수 밖에 없고, 제대로 하지 않는다면 흐름을 제대로 파악하기 힘들 수도 있다.

마지막으로 Alamofire를 한번이라도 사용해 봤다면 유닛테스트를 하기 위해서 아주 귀찮은 작업이 동반된다는 사실을 알것이다. (참고)

과연 Moya는 어떠한 방식으로 위와 같은 문제를 해결하였는지, 알아보도록 하자.

How is Moya structured?

Provider

Provider는 네트워킹 요청을 발생시키는 객체로, 애플리케이션 내에서 실제 HTTP 요청을 수행하는 데 필요한 모든 정보를 캡슐화한다.

위를 보면 MoyaProvider를 구성하기 위해서는 Target 설정이 필요하다.

Target

Target은 API 엔드포인트를 나타내는 것을 말한다. Target을 정의하기 위해서는 Moya의 TargetType 프로토콜을 준수해야 한다.

  • baseURL: 요청을 보낼 기본 URL을 나타낸다.
  • path: 기본 URL에 추가될 경로를 나타낸다.
  • method: HTTP 메소드(예: GET, POST, PUT 등)를 나타낸다.
  • task: 요청에 사용될 작업(예: 요청 파라미터, 업로드 파일 등)을 나타낸다.
  • headers: 요청에 포함될 HTTP 헤더를 나타낸다.
  • validationType : 허용할 response 정의를 나타낸다. (필수 X)
  • sampleData: 테스트 용도로 사용될 샘플 데이터를 제공한다. (필수 X)

RequestClosure

requestClosure는 Moya의 Provider가 네트워크 요청을 실행하기 전에, 최종적으로 URLRequest를 구성하거나 변경할 수 있는 기능을 제공한다. 이를 통해 개발자는 요청을 전송하기 직전에 필요한 추가적인 설정이나 로직을 적용할 수 있다.

RequestClosure의 사용 예시

  • 동적 헤더 추가: API 키나 토큰과 같은 인증 정보를 헤더에 추가하거나, 요청마다 다를 수 있는 다른 헤더 값을 설정할 때 사용된다.
  • 요청 매개변수 수정: 요청을 전송하기 전에 URL 매개변수나 바디 매개변수를 수정하거나 추가할 때 사용된다.
  • 조건부 요청 로직 적용: 특정 조건에 따라 요청을 수정하거나, 특정 요청을 전송하지 않도록 할 때 사용된다.
  • 디버깅 및 로깅: 네트워크 요청을 전송하기 직전에 요청 정보를 로깅하는 용도로 사용될 수 있다.
let provider = MoyaProvider<MyTarget>(requestClosure: { (endpoint, done) in
do {
var request = try endpoint.urlRequest()
// 요청 수정 로직 (예: 헤더 추가)
request.addValue("my-api-key", forHTTPHeaderField: "Authorization")
// 수정된 요청을 완료 클로저에 전달
done(.success(request))
} catch {
done(.failure(MoyaError.underlying(error, nil)))
}
})

Plugin

Moya에서 Plugin은 네트워크 요청의 생명주기 동안에 발생하는 이벤트에 대응하여 추가적인 작업을 수행할 수 있게 해주는 구성요소이다.

주요 PluginType 메서드

  • willSend: 요청이 전송되기 직전에 호출된다. 이 메서드를 통해 요청에 관한 정보를 로깅하거나, 요청을 수정할 수 있다.
  • didReceive: 응답을 받은 직후에 호출된다. 응답 데이터를 가공하거나, 응답에 대한 로깅 등의 작업을 수행할 수 있다.
  • prepare: 요청이 전송되기 전, URLRequest를 최종적으로 수정할 수 있는 기회를 제공한다. 예를 들어, 공통적인 헤더를 추가하는 등의 작업을 수행할 수 있다.
  • process: 응답을 받고, 해당 응답을 가공하거나 추가적인 처리를 하기 위해 호출된다. 예를 들어, 응답으로부터 특정 데이터를 추출하거나 변환하는 작업을 수행할 수 있다.

네트워크 요청과 응답을 로깅하기 위한 간단한 플러그인 예시-

final class NetworkLoggerPlugin: PluginType {
func willSend(_ request: RequestType, target: TargetType) {
print("\nSending request: \(request.request?.url?.absoluteString ?? "")")
}

func didReceive(_ result: Result<Response, MoyaError>, target: TargetType) {
switch result {
case .success(let response):
print("\nReceived response: \(response.statusCode) from \(response.request?.url?.absoluteString ?? "")")
case .failure(let error):
print("\nRequest failed with error: \(error)")
}
}

How to Use?

아래와 같은 조건을 가진 유저 정보를 가져오는 API 통신을 한다고 가정하자.

  1. 유저 정보를 가져오기 위해 Header에 AccessToken을 가지고 있어야한다.

2. 제공 받은 Response Header에 RefreshToken이 존재 한다면 Keychain에 저장한다.

1. TargetType을 채택하는 API 객체를 생성한다.

import Foundation
import Moya

struct UserInfoAPI: TargetType {
let userId: String

var baseURL: URL {
return URL(string: "https://api.example.com")!
}

var path: String {
return "/users/\(userId)"
}

var method: Moya.Method {
return .get
}

// 단순한 get 요청만이 필요함으로 가장 기본적인 .requestPlain으로 설정한다.
var task: Task {
return .requestPlain
}

var headers: [String : String]? {
return ["Content-Type": "application/json"]
}

// 샘플 데이터는 테스트나 스텁에 사용될 수 있다.
// 여기서는 간단한 예제를 위해 빈 데이터를 반환한다.
var sampleData: Data {
return Data()
}
}

2. AcessToken을 RequestHeader에 추가하는 RequestClosure를 만든다.


//보통 kechain으로 부터 가져온다.
let accessToken = "your_access_token_here"

let requestClosure = { (endpoint: Endpoint, done: @escaping MoyaProvider.RequestResultClosure) in
do {
var request = try endpoint.urlRequest()
// accessToken을 헤더에 추가
request.addValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
// 수정된 요청을 완료 클로저에 전달
done(.success(request))
} catch {
done(.failure(MoyaError.underlying(error, nil)))
}
}

3. Response Header에 RefreshToken이 존재 한다면 Keychain에 저장하는 Plugin을 생성한다.

import Moya

struct RefreshTokenStoragePlugin: PluginType {
func didReceive(_ result: Result<Moya.Response, MoyaError>, target: TargetType) {
guard case .success(let response) = result else {
return
}

// 응답 헤더에서 "RefreshToken" 찾기
if let refreshToken = response.response?.allHeaderFields["RefreshToken"] as? String {
// Keychain에 RefreshToken 저장
do {
KeyChainManager.shared.addItemsOnKeyChain(refreshToken)
print("RefreshToken was successfully stored in the Keychain.")
} catch {
print("Failed to store RefreshToken in the Keychain.")
}
}
}
}

4. Provider를 생성하여 실질적인 네트워킹을 진행한다.

func fetchUserInfo(userId: String) {
let provider = MoyaProvider<UserInfoAPI>(requestClosure: requestClosure, plugins: [RefreshTokenStoragePlugin()])

provider.request(.getUserInfo(userId: userId)) { result in
switch result {
case .success(let response):
// 성공적으로 데이터를 받았을 때의 처리 로직
print("Received user data: \(response.data)")
case .failure(let error):
// 에러 처리 로직
print("Error occurred: \(error)")
}
}
}

위와 같이 코드를 작성하였을때 아래와 같은 흐름이 된다.

  1. UserInfoAPI라는 구조체에 TargetType을 채택하여, Provider의 생성에 필요한 Target을 구성한다.
  2. requestClosure를 통해 accessToken을 헤더에 추가한 URLRequest를 최종적으로 구성한다.
  3. Provider의 Target을 UserInfoAPI 설정하고, 위에서 구현한 requestClosure 및 plugin을 주입받은 Provider 인스턴스를 생성하면, 해당 Provider를 통해 네트워킹을 할 수 있게 된다.
  4. plugin을 통해 response를 받은 이후(didReceive 메서드 사용) refreshToken의 유무에 따른 Keychain 저장을 진행한다.

결론

내가 생각한 Moya의 장점은 아래와 같다.

TargetType을 채택한 객체를 통해 API 요청에 대한 캡슐화 및 구조파악에 용이함을 가져올 수 있다.

또한 이미 잘 추상화 된 라이브러리 이므로 네트워크 구조에 대한 고민을 줄여줌으로써 프로젝트의 구성에 시간을 덜 소모 할 수있다.

enum 타입의 사용으로 코드 안정성이 올라간다.

또한, provider의 인스턴스를 생성할때 stubClosure을 사용하여 간편하게 테스트 할수 있다. (sampleData를 반환)

위와 같은 이유로 처음 언급한 문제점들은 해결이 된다.

그냥 훨신 코드가 깔끔해진다!

--

--