오늘은 Alamofire에 대해서 알아보려고 한다. 사실 야곰아카데미에서는 오픈소스, 라이브러리를 사용하기보단 원리를 이해하고 기초를 알길 권장하기 때문에 한번도 사용해본적이 없었다. 근데 Alamofire가 채용공고에 자주 등장하기도 하고 어떤라이브러리인지 궁금해서 이번기회에 Alamofire에 대해 알아보고, HighwayInfo 프로젝트에 적용해보고자 한다.
Alamofire는 애플의 서버 통신 API인 URLSession을 기반으로 더 편리한 네트워킹을 도와주는 라이브러리이다.
메서드 1 | 메서드 2 |
---|---|
이렇게 파라미터만 다른 중복되는 두 메서드를 리팩토링 해보고자 합니다.
우선 가장 먼저 만들어볼것은 서버 통신 규약을 정의하는 Router 프로토콜과 해당 프로토콜을 따르면서 실제 서버 통신을 담당하는 RouterManager를 만들어보겠습니다.
Router는 네트워크 통신에 필수요소인 URL과 path, HTTP 통신 방법, 헤더, 그리고 데이터 요청 업무인 task 통 5가지를 가지도록 선언해주었습니다.
import Alamofire
protocol Router {
var baseURL: URL { get }
var path: String { get }
var method: HTTPMethod { get }
var headers: [String: String]? { get }
var task: Task { get }
}
enum Task {
case requestPlain
case requestJSONEncodable(Encodable)
case requestParameters(parameters: [String: Any])
}
그 다음으로 해당 프로토콜을 채택해 실제 통신을 구현하는 RouterManager를 만들어보겠습니다.
RouterManager는 Session과 Interceptor를 가집니다.
request
메서드를 호출하면 통신이 이루어지고
메서드의 실구현은 extension으로 빼서 가독성을 높여보았습니다.
에러에 대해서도 커스텀한 에러 프로토콜을 만들고(RouterManagerError
) 채택해 서비스에 맞는 에러 형태를 띠도록 구현해보았습니다.
struct RouterManager<T: Router> {
private let alamofireSession: Session
private let interceptor: Interceptor?
init(alamofireSession: Session = .default, interceptor: Interceptor? = nil) {
self.alamofireSession = alamofireSession
self.interceptor = interceptor
}
func request(router: T) -> Single<Data> {
return sendRequest(router: router)
}
}
sendRequest 메서드를 보면
extension RouterManager {
func sendRequest(router: T) -> Single<Data> {
return Single.create(subscribe: { single in
let dataRequest: DataRequest = makeDataRequest(router: router)
dataRequest
// response.result로 응답의 결과를 알 수 있음
.responseData { response in
// switch를 사용해서 결과에 따른 클로져를 작성
switch response.result {
case .success:
guard let statusCode = response.response?.statusCode else { return }
let isSuccessful = (200..<300).contains(statusCode)
if isSuccessful {
guard let data = response.data else { return }
single(.success(data))
} else {
let error = RouterManagerError(code: .isNotSuccessful, response: response)
single(.failure(error))
}
// 요청이 실패하는 경우 .failure과 error를 전달
case .failure(let underlyingError):
let error = RouterManagerError(
code: .failedRequest,
underlying: underlyingError,
response: response
)
single(.failure(error))
}
}
return Disposables.create()
})
}
}
import Alamofire
enum AccidentAPI {
// api 목록
case getAccidents
}
extension AccidentAPI: Router {
// base url
var baseURL: URL {
return URL(string: "http://openapigits.gg.go.kr/api/rest/")!
}
// base url 뒤에 붙는 path
var path: String {
switch self {
case .getAccidents:
return "getIncidentInfo?"
}
}
// API 요청방식
var method: HTTPMethod {
switch self {
case .getAccidents:
return .get
}
}
// API 요청 헤더
var headers: [String: String]? {
return nil
}
// 인코딩 방식
// 파라미터로 보낼야할 것이 있으면 .requestParameters
// 바디에 담아서 보내야할 것이 있으면 .requestJSONEncodable
var task: Task {
switch self {
case .getAccidents:
return .requestParameters(
parameters: ["serviceKey": Bundle.main.accidentApiKey]
)
}
}
}
실제 API통신을 수행하는 Service 객체를 만들어보자
import Foundation
import RxSwift
struct AccidentService {
let fetchAccidents: () -> Observable<[AccidentDTO]>
init(fetchAccidents: @escaping () -> Observable<[AccidentDTO]>) {
self.fetchAccidents = fetchAccidents
}
}
extension AccidentService {
static let live = Self(
fetchAccidents: {
return RouterManager<AccidentAPI>
.init()
.request(router: .getAccidents)
.map({ data in
let parser = AccidentParser(data: data)
let decoded = parser.parseXML()
return decoded
})
.asObservable()
}
)
}
final class DefaultAccidentRepository: AccidentRepository {
private let service: AccidentService
init(service: AccidentService) {
self.service = service
}
func fetchAllAccidents() -> Observable<[AccidentDTO]> {
service.fetchAccidents()
}
}
Alamofire를 적용하면서 이렇게 중복되는 메서드를 리팩토링해보았습니다. RouterManager를 보면 기존에 두번 반복되던 코드가 사라진걸 확인해볼 수 있어요.
앱에서 수많은 API 요청이 발생하는데, Request Router를 통해 모든 요청을 생성할 수 있고, 이를 통해 하나의 파일에서 일관적으로 관리할 수 있었어요.
기존에 여기저기 흩어져 있는 통신관련 파일들을 하나로 볼 수 있어서 가독성이 높아졌고, 유지보수하기도 수월해진것같아요.