Processing URL Session Data Task Results with Combine

Horus-iOS·2023년 9월 21일
0
post-custom-banner

https://developer.apple.com/documentation/foundation/urlsession/processing_url_session_data_task_results_with_combine

Use a chain of asynchronous operators to receive and process data fetched from a URL.

URL로부터 받아온 데이터를 수신하고 처리하기 위해 비동기 오퍼레이터의 연쇄를 사용할 수 있습니다.

Overview

URL 세션을 사용하는 작업 수행은 본질적으로 비동기입니다. 네트워크 엔드포인트, 파일 시스템, 그리고 기타 URL 기반 소스로부터 데이터를 받아오는 것은 시간이 소요됩니다. URL 로딩 시스템은 딜리게이트 혹은 컴플리션 핸들러를 통해 비동기적으로 결과를 가져오기 위한 책임이 있습니다. 컴바인 프레임워크 역시 비동기를 다룹니다. URL 작업을 처리하기 위해 사용하면 결과를 단순화 하고 더 나은 코드를 작성할 수 있습니다.

Create a Data Task Publisher

URLSession은 컴바인 퍼블리셔를 제공합니다. URLSession.DataTaskPublisher이며, URL, URLRequest로부터 데이터를 불러올 때 결과를 퍼블리시합니다. dataTaskPublisher(for:) 메소드를 사용해서 이와 같은 퍼블리셔를 생성할 수 있습니다. 작업이 완료되면 아래 둘 중 한 가지를 퍼블리시합니다.

  • 작업이 성공한 경우 받아온 데이터와 URLResponse를 포함하는 튜플입니다.
  • 작업이 실패한 경우 에러입니다.

dataTask(with:completionHandler:)로 전달하는 컴플리션 핸들러와 달리 코드로 받아온 타입은 옵셔널이 아니며, 퍼블리셔가 이미 데이터 혹은 에러를 언랩하기 때문입니다.

URLSession의 컴플리션 핸들러 기반 코드를 사용하면 오류 처리, 데이터 파싱 기타 등 필요한 모든 것을 핸들러 클로저에서 처리해야 합니다. 퍼블리셔를 사용하면 여러 처리에 대한 책임을 컴바인 오퍼레이터로 옮길 수 있습니다.

Convert Incoming Raw Data to Your Types with Combine Operators

데이터 작업이 성공적으로 완료되면 데이터의 블록을 앱으로 전달합니다. 대부분의 앱은 이 데이터를 앱이 사용하고 있는 타입으로 바뀌어야 합니다. 컴바인은 이와 같은 변환을 수행하기 위한 오퍼레이터를 제공하고 있으며, 오퍼레이션 처리를 가능하게 하는 오퍼레이터 연쇄를 선언할 수 있도록 해줍니다.

데이터 테스크 퍼블리셔는 데이터와 URLResponse가 포함된 튜플을 제공합니다. map(_:) 오퍼레이터를 사용해서 이 튜플을 다른 타입으로 변환할 수 있습니다. 데이터를 확인하기 전에 리스폰스를 확인하려면 tryMap(_:)을 사용해서 리스폰스가 적합하지 않을 때 에러를 내보낼 수 있습니다.

데이터를 Decodable 프로토콜을 따르는 타입으로 변환하려면 컴바인의 decode(type:decoder:) 오퍼레이터를 사용할 수 있습니다.

아래 예시는 URL 엔드포인트로부터 받은 JSON 데이터를 사용자의 커스텀 타입으로 파싱하는 예시입니다.

struct User: Codable {
    let name: String
    let userID: String
}
let url = URL(string: "https://example.com/endpoint")!
cancellable = urlSession
    .dataTaskPublisher(for: url)
    .tryMap() { element -> Data in
        guard let httpResponse = element.response as? HTTPURLResponse,
            httpResponse.statusCode == 200 else {
                throw URLError(.badServerResponse)
            }
        return element.data
        }
    .decode(type: User.self, decoder: JSONDecoder())
    .sink(receiveCompletion: { print ("Received completion: \($0).") },
          receiveValue: { user in print ("Received user: \(user).")})

Retry Transient Errors and Catch and Replace Persistent Errors

네트워크를 사용하는 모든 앱은 에러를 만나게 되는 경우에 대비해야 합니다. 그리고 앱은 발생한 에러를 잘 처리해야 합니다. 일시적인 네트워크 에러 발생은 흔하기 때문에 실패한 데이터 테스크를 즉시 재시도하길 원할 것입니다. URLSession의 컴플리션 핸들러를 사용하면 재시도를 수행하기 위한 모든 새 작업을 생성해야 합니다. 데이터 테스크 퍼블리셔를 사용하면 컴바인의 retry(_:) 오퍼레이터를 사용할 수 있습니다. 이 오퍼레이터는 업스트림 퍼블리셔에게 구체적인 횟수로 구독을 재생성해서 오류를 처리합니다. 그러나 네트워크 작업은 비용이 높기 때문에 적은 수만큼만 재시도해야 하고, 모든 요청이 멱등성을 띄도록 해야 합니다.

에러를 대체하기 위한 컴바인 오퍼레이터를 사용할 수도 있습니다.

  • catch(_:) 오퍼레이터는 에러를 다른 퍼블리셔로 대체합니다. 예를 들어 폴백 URL로부터 데이터를 불러오는 URLSession.DataTaskPublisher를 사용할 수 있습니다.
  • replaceError(with:) 오퍼레이터는 에러를 직접 생성한 요소로 대체할 수 있습니다. 그렇게 하는 것이 앱에서 합리적이라면, URL로부터 불러올 때 기대하고 있었던 값으로 대체하기 위해 이 오퍼레이터를 사용할 수 있습니다.

아래 예시는 위에서 소개한 테크닉을 모두 포함하고 있습니다. 실패한 리퀘스트를 한 번 재시도하고, 이후 폴백 URL을 사용합니다. 첫 리퀘스트, 재시도, 폴백 리퀘스트 중 하나라도 성공하면 sink(receiveValue:) 오퍼레이터가 엔드포인트로부터 데이터를 받습니다. 모두 실패하면 sink는 Subscribers.Completion.failure(_:)를 받습니다.

let pub = urlSession
    .dataTaskPublisher(for: url)
    .retry(1)
    .catch() { _ in
        self.fallbackUrlSession.dataTaskPublisher(for: fallbackURL)
    }
cancellable = pub
    .sink(receiveCompletion: { print("Received completion: \($0).") },
          receiveValue: { print("Received data: \($0.data).") })

Move Work Between Dispatch Queues With Scheduling Operators

URLSession의 딜리게이트와 컴플리션 핸들러를 사용하면 세션은 고정된 delegateQueue에 코드로 콜백합니다. 이 부분은 간혹 콜백 코드가 수동적으로 디스패치 큐를 사용해야만 한다거나 혹은 특정 큐에 작업을 넣을 수 있도록 다른 스케줄링 API를 사용해야 함을 의미합니다.

URLSession.DataTaskPublisher를 사용하면 컴바인의 스케줄링 오퍼레이터를 대체재로 사용할 수 있습니다. 작업을 스케줄링하고자 뒤에 이어지는 오퍼레이터와 구독자를 얼마나 원하는지를 구체화하기 위해 receive(on:options:)를 사용할 수 있습니다. DispatchQueueRunLoop 모두 컴바인의 Scheduler 프로토콜을 구현하고 있으니 URL 세션 데이터를 받기 위해 둘을 사용할 수 있습니다. 아래 코드는 sink가 메인 디스패치 큐에서 결과를 기록하는지 확인합니다.

cancellable = urlSession
    .dataTaskPublisher(for: url)
    .receive(on: DispatchQueue.main)
    .sink(receiveCompletion: { print ("Received completion: \($0).") },
          receiveValue: { print ("Received data: \($0.data).")})

Share the Result of a Data Task Publisher with Multiple Subscribers

앱의 여러 부분에서 URL 엔드포인트로부터 데이터를 사용하길 원하는 경우도 있을 수 있습니다. 네트워크 리퀘스트는 비용이 높기 때문에 불필요하게 이슈를 발생시킬 필요가 없습니다. 컴바인은 퍼블리셔가 한 번의 리퀘스트를 사용함에도 단일 URLSession.DataTaskPubliser로 여러 구독자를 사용할 수 있도록 지원합니다.

여러 다운스트림 구독자를 지원하려면 share() 오퍼레이터를 사용할 수 있습니다. 이 오퍼레이터는 Publishers.Multicast, PassthroughSubject 퍼블리셔의 조합처럼 동작합니다. 여러 오퍼레이터의 연쇄 혹은 구독자를 share() 오퍼레이터에 연결시킬 수 있고, 모든 업스트림 퍼블리셔가 하나의 다운스트림만 보도록 할 수 있습니다. URLSession.DataTaskPublisher의 경우 데이터 테스크는 한 번만 수행한다는 것을 의미합니다.

아래 예시는 URL 세션 데이터 테스크를 관련성이 없는 두 가지 목적을 위해 사용합니다. 하나의 구독자는 받은 데이터를 커스텀 사용자 타입으로 파싱하기 위해 사용하고 메인 디스패치 큐에 기록합니다. 두 번재 구독자는 HTTP 상태 코드를 조사하고자 프린트하려는 목적으로 오직 URLResponse만 신경쓰고 있습니다. 그리고 어떤 큐에서 사용하는지는 관심이 없습니다. share()를 사용하면 데이터 테스크 퍼블리셔는 URL 엔드포인트를 한 번 로드하면서 동시에 모든 구독자에게 서비스를 제공할 수 있습니다.

let sharedPublisher = urlSession
    .dataTaskPublisher(for: url)
    .share()


cancellable1 = sharedPublisher
    .tryMap() {
        guard $0.data.count > 0 else { throw URLError(.zeroByteResource) }
        return $0.data
    }
    .decode(type: User.self, decoder: JSONDecoder())
    .receive(on: DispatchQueue.main)
    .sink(receiveCompletion: { print ("Received completion 1: \($0).") },
          receiveValue: { print ("Received id: \($0.userID).")})


cancellable2 = sharedPublisher
    .map() {
        $0.response
    }
    .sink(receiveCompletion: { print ("Received completion 2: \($0).") },
           receiveValue: { response in
            if let httpResponse = response as? HTTPURLResponse {
                print ("Received HTTP status: \(httpResponse.statusCode).")
            } else {
                print ("Response was not an HTTPURLResponse.")
            }
    }
)

이를 증명하려면, 이 코드는 데이터를 한 번만 불러오고, 일시적으로 print(_:to:)를 통해 share() 오퍼레이터 이전에 디버깅을 합니다. 앱을 실행하면 Xcode의 콘솔 출력이 데이터 테스크 퍼블리셔로부터 오직 하나의 값만 받는다는 것을 보여주며, 그럼에도 모든 구독자는 기대한 결과를 받게 됩니다.

URL 세션은 URLSession.DataTaskPublisher가 다운스트림 구독자의 요구사항이 충족되지 않는 즉시 데이터 로딩을 시작한다는 것을 주의해야 한비다. 이 경우 첫 번째 sink 구독자가 붙을 때 발생합니다. 다른 구독자를 붙이기 위해 여분의 시간이 필요하다면 makeConnectable()를 사용해서 ConnectablePublisher를 이용해 Publishers.Share 퍼블리셔로 감쌀 수 있습니다. 모든 구독자를 연결한 후 데이터 불러오기 작업을 시작할 수 있도록 연결 가능한 퍼블리셔에 connect()를 호출해야 합니다.

post-custom-banner

0개의 댓글