URLSession
URLSession은 네트워크 작업을 처리하기 위해 구현된 클래스입니다.
저는 프로젝트에서 에러 핸들링 과정과 함께 URLSession을 아래와 같이 사용했습니다.
// URLSession NetworkError
enum NetworkError: Error {
case invalidUrl
case transportError
case serverError(code: Int)
case missingData
case decodingError(error: Error)
}
// URLSessionConfiguration
private let session = URLSession(configuration: .default)
// 반환 타입 정의
typealias NetworkResult = (Result<WeatherDataModel, NetworkError>) -> ()
// URLSession GET Data
func dataFetch(nx: Int, ny: Int, convenience: Bool, completion: @escaping NetworkResult) {
// URL 확인
let urls = getUrl(convenience: convenience, nx: nx, ny: ny)
for url in urls {
guard let url = url.url else {
completion(.failure(.invalidUrl))
return
}
var request: URLRequest = URLRequest(url: url)
request.httpMethod = "GET"
let dataTask = session.dataTask(with: request) { data, response, error in
// 연결 확인
guard error == nil else {
completion(.failure(.transportError))
print(error?.localizedDescription)
return
}
// 서버 확인
let successRange = 200..<300
guard let statusCode = (response as? HTTPURLResponse)?.statusCode else { return }
if !successRange.contains(statusCode) {
completion(.failure(.serverError(code: statusCode)))
return
}
// 데이터 확인
guard let loadData = data else {
completion(.failure(.missingData))
return
}
// 디코딩
do {
let parsingData: WeatherDataModel = try
JSONDecoder().decode(WeatherDataModel.self, from: loadData)
completion(.success(parsingData))
} catch let error {
completion(.failure(.decodingError(error: error)))
}
}.resume()
}
}
최신기술?인 async await을 사용하지않고 escaping 클로저로 비동기 데이터 통신을 처리했습니다.
절차는 아래와 같습니다!
- URL 생성 (URL생성에 문제가 생기면 오류리턴)
- 생성된 URL을 바탕으로 URLComponent를 만들고, URLRequest 생성
- URLSession을 통해 데이터 통신 (클로저 안에서 진행되고, 리턴값은 escaping 선언에 따라 해당 함수가 종료되면 실행)
- 네트워크 연결, 서버연결, 데이터불러오기 확인절차를 모두 진행하고 실패하면 오류 리턴
- 위 사항까지 모두 정상적으로 진행되었다면 디코딩 진행(JSON 모델과 받을 데이터의 모델과 형식이 같은지 주의)
- 디코딩이 완료되면 해당 데이터를 리턴
이렇게 작성하고 데이터를 받는 부분에서 불러올땐 클로저를 통해 데이터를 받았습니다.
APIManager.shared.dataFetch(nx: location.x, ny: location.y, convenience: false) {[weak self] result in
guard let self = self else { return }
switch result {
case .success(let data):
self.convertDataFromCategory(response: data)
case .failure(.decodingError(error: let error)):
print("decodingError: \(error.localizedDescription)")
case .failure(.invalidUrl):
print("invalidURL")
case .failure(.missingData):
print("missingData")
case .failure(.serverError(code: let code)):
print("serverError \(code)")
case .failure(.transportError):
print("transportError")
}
}
이렇게 네트워크 통신작업을 처리했는데 이것을 RxSwift로 리팩토링 하고자 합니다.
RxSwift URLSession
RxSwift는 반응형 프로그래밍으로 이전에 작성했던 코드와는 다른 개념이지만, RxSwift는 Swift에서 사용하던 UI나 프로토콜등 많은 프레임워크가 Reactive 형식으로 확장되서 지원하고있습니다.
URLSession도 Swift나 Objective-C로 작성되어 사용되었지만 Rx버전으로도 확장되었습니다.
extension Reactive where Base: URLSession {
public func response(request: URLRequest) -> Observable<(response: HTTPURLResponse, data: Data)> {
return Observable.create { observer in
let d: Date?
// shouldLogRequest: RxCocoa확장 라이브러리를 사용할 때, 네트워크 요청 로깅을 제어하기 위해 사용하는 함수?
// 디버깅 목적으로 사용
// 정확한 역할은 모르지만,, 코드 흐름을 보고 유추하자면.. 라이브러리 네트워크 연결이 됬다면 true를 리턴하는듯 하다.
if URLSession.rx.shouldLogRequest(request) {
d = Date()
}
else {
d = nil
}
// URLSession Task
let task = self.base.dataTask(with: request) { data, response, error in
if URLSession.rx.shouldLogRequest(request) {
let interval = Date().timeIntervalSince(d ?? Date())
// HTTPMethod, URLRequest의 url문자열 출력 (올바른 url이 아니면 "<unknown url>"
print(convertURLRequestToCurlCommand(request))
#if os(Linux)
print(convertResponseToString(response, error.flatMap { $0 as NSError }, interval))
#else
// 네트워크 연결상태 확인 status code (200..<300)
print(convertResponseToString(response, error.map { $0 as NSError }, interval))
#endif
}
// 응답오류, 데이터 불러오기 오류
guard let response = response, let data = data else {
observer.on(.error(error ?? RxCocoaURLError.unknown))
return
}
// HTTP응답없음 오류 리턴
guard let httpResponse = response as? HTTPURLResponse else {
observer.on(.error(RxCocoaURLError.nonHTTPResponse(response: response)))
return
}
// 응답값과 데이터 리턴 후 종료
observer.on(.next((httpResponse, data)))
observer.on(.completed)
}
task.resume()
// Observable Dispose
return Disposables.create(with: task.cancel)
}
}
}
※ 코드 분석
- URLRequest를 인자로 받아 Observable<(response, data)> 형식으로 리턴한다.
- 라이브러리와 네트워크 통신을 디버깅목적으로 확인하는URLSession.rx.shouldLogRequest(request)을 거친다.
- 서버 통신확인, 서버로부터 응답과 url응답 확인한다. 오류가 발생한다면 오류 이벤트를 방출한다.
- 정상적으로 통신이 됬다면 해당 http응답값과 data를 이벤트 방출하고 종료한다.
URLResponse: 네트워크요청에 대한 응답을 나타낸다.
HTTPResponse: HTTP프로토콜을 사용한 네트워크에 대한 응답을 나타낸다. (URLResponse의 서브클래스)
사용 예시
func getData(nx: Int, ny: Int, convenience: Bool) -> Observable<WeatherDataModel> {
// components의 리턴값은 Observable<URLComponents>
return components(nx: nx, ny: ny, convenience: convenience)
// URLComponents를 URLRequest로 변환
.map{ url in
return URLRequest(url: url.url!)
}
// 변환된 URLRequest를 통해 통신
// URLSession.shared.rx.response에는 이미 에러대응이 구현되어있음.
// 리턴값은 Observable<(response: HTTPURLResponse, data: Data)>
.flatMap { request -> Observable<(response: HTTPURLResponse, data: Data)> in
return URLSession.shared.rx.response(request: request)
}
// response, data를 인자로 받고 정한 데이터모델로 디코딩 (실패시 임의의 값 넣기)
.map{ _, data -> WeatherDataModel in
let decoder = JSONDecoder()
let decoded = try? decoder.decode(WeatherDataModel.self, from: data)
return decoded ?? WeatherDataModel(response: DataResponse(header: ResponseHeader(resultCode: "04"), body: nil))
}
}
RxURLSession은 에러대응이 구현되어 있지만, 디코딩 에러에 대한 대응은 따로 구현해줘야 합니다.
또한 리턴값이 Observable이지만 네트워크 통신같은 경우는 성공과 실패의 결과만 있어도 되므로 Traits중 하나인 Single을 리턴하도록 합니다.
Single
Observable보다 더 좁은 범위로 .success 와 .error만 방출한다.
데이터 로딩과 같은 1회성 프로세스에 적합하다.
Observable에서 asSingle로 변환할 수 있다.
typealias NetworkResult = Result<WeatherDataModel, NetworkError>
func example(nx: Int, ny: Int, page: Int) -> Single<NetworkResult> {
return Single<NetworkResult>.create {[weak self] single in
// URL 확인
guard let components = self?.component(nx: nx, ny: ny, page: page) else {
single(.failure(NetworkError.invalidUrl))
return Disposables.create()
}
let request = URLRequest(url: components.url!)
URLSession.shared.dataTask(with: request) { data, response, error in
// 에러 확인
if let error = error {
single(.failure(NetworkError.transportError))
return
}
let successRange = 200..<300
guard let statusCode = (response as? HTTPURLResponse)?.statusCode else { return }
// 접속 코드 확인
if !successRange.contains(successRange) {
single(.failure(NetworkError.serverError(code: statusCode)))
return
}
// 데이터 확인
guard let loaded = data else {
single(.failure(NetworkError.missingData))
return
}
// 디코딩 확인
do {
let decoded = try JSONDecoder().decode(WeatherDataModel.self, from: loaded)
single(.success(NetworkResult.success(decoded)))
} catch {
single(.failure(NetworkError.decodingError))
}
}.resume()
return Disposables.create()
}
}