Combine์— ๋Œ€ํ•ด์„œ ์‹ค์ œ ์‚ฌ์šฉํ•˜๋‹ค ๋ณด๋‹ˆ, ์ด๊ฒŒ ๋„๋Œ€์ฒด ์–ด๋–ค ๋ฐฉ์‹์œผ๋กœ ์ด๋ฃจ์–ด์ง€๋Š” ์ง€ ๊ถ๊ธˆํ–ˆ๋‹ค. ์‹ค์ œ ์ฝ”๋“œ๋ฅผ ๋ณด์ง€ ์•Š์œผ๋ฉด ๋ช…ํ™•ํ•ด์ง€์ง€ ์•Š์„ ๊ฒƒ ๊ฐ™์•„ ์ •๋ฆฌํ•œ๋‹ค.

Objective

URLSession์—์„œ ์š”์ฒญ์„ ๋ฐ›์•„ ๋‚ด๊ฐ€ ์›ํ•˜๋Š” ์‘๋‹ต์„ ๋ณด๋‚ด์ฃผ๋Š” Publisher, ๊ทธ์— ํ•ด๋‹นํ•˜๋Š” Subscriber, Subscription์„ ๋ชจ๋‘ ๋”ฐ๋กœ ๋งŒ๋“ค์–ด ๋ณธ๋‹ค.

์ด์ „ ๊ธ€์—์„œ ๋ฐฐ์› ๋˜ ์ด ํ๋ฆ„์„ ๊ธฐ๋ฐ˜์œผ๋กœ, Custom Publisher, Subscriber๋ฅผ ๋งŒ๋“ค์–ด๋ณด๋ฉด์„œ ๋‚ด๋ถ€ ๋™์ž‘์„ ์ดํ•ดํ•ด๋ณด๋Š” ๊ฒƒ์„ ๋ชฉํ‘œ๋กœ ํ•œ๋‹ค. ๊ทธ ์ „์—, ์•ž์˜ ๊ธ€๊ณผ ์กฐ๊ธˆ ๋‹ค๋ฅธ ๋ถ€๋ถ„์ด ์žˆ์–ด ๊ทธ๋ฆผ์„ ์ˆ˜์ •ํ–ˆ๋‹ค. WWDC์—์„œ ๋‚˜์˜จ ์ด ๊ทธ๋ฆผ์€ ์–ผํ• ๋ณด๋ฉด Publisher์—๊ฒŒ ๊ฐ’์„ ์š”์ฒญํ•˜๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ๋ณด์ธ๋‹ค. ํ•˜์ง€๋งŒ ๊ทธ๊ฒƒ์€ ์‚ฌ์‹ค์ด ์•„๋‹ˆ๋ฉฐ, Publisher๊ฐ€ Subscription์„ ์ธ์ž๋กœ ๋„˜๊ฒจ์ฃผ๋Š” ์‹œ์ ๋ถ€ํ„ฐ, Subscription ์ธ์Šคํ„ด์Šค์—์„œ Subscriber์˜ Method๋“ค์„ ํ˜ธ์ถœํ•ด ์ค€๋‹ค. ๊ทธ๋ž˜์„œ Subscription ๊ฐ์ฒด ๊ทธ๋ฆผ์„ ํ•˜๋‚˜ ์ถ”๊ฐ€ํ–ˆ๋‹ค. ์–ด๋–ป๊ฒŒ Customdmfh Publisher, Subscriber๋ฅผ ๋งŒ๋“œ๋Š”์ง€ ๋”ฐ๋ผ๊ฐ„๋‹ค๋ฉด ์ดํ•ดํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ์ด๋‹ค.

๊ทธ์ „์—, ๊ตฌํ˜„์„ ์–ด๋–ค ๋ฐฉ์‹์œผ๋กœ ํ• ์ง€์— ๋Œ€ํ•ด ๊ฐ„๋‹จํ•˜๊ฒŒ ์•Œ์•„๋ณด์ž.

  1. Publisher๋Š” URLSession์˜ extension์— ์œ„์น˜ํ•ด์„œ ๋งŒ๋“ ๋‹ค.
  2. Subscriber๋Š” ๊ทธ๋ƒฅ ๋งŒ๋“ค์–ด์„œ ๊ด€๋ฆฌํ•œ๋‹ค.
  3. Publisher ์ž…์žฅ์—์„œ subscriber๊ฐ€ ์ธ์ˆ˜๋กœ ๋“ค์–ด์™”์„ ๋•Œ, Subscriber์˜ receive(subscription:)์„ ํ˜ธ์ถœํ•ด์ฃผ์–ด์•ผ ํ•˜๊ธฐ ๋•Œ๋ฌธ์—, Subscription ๊ฐ์ฒด๋Š” Publisher ๋‚ด๋ถ€์—์„œ ์ •์˜ํ•˜๋„๋ก ํ•œ๋‹ค.

๊ทธ๋Ÿผ ์‹œ์ž‘ํ•ด๋ณด์ž.

Custom Subscriber

์ผ๋‹จ Subscriber๋ฅผ ๋งŒ๋“ค์–ด๋ณด์ž. WWDC์˜ ํ๋ฆ„๋„๋ฅผ ๋ณธ๋‹ค๋ฉด, Subscriber์—์„œ๋Š” Publisher์— ์ž์‹ ์„ ๋ณด๋‚ด๋Š” method, input์„ ๋ฐ›๋Š” method, ๋๋‚ฌ์„ ๋•Œ ํ˜ธ์ถœ๋˜๋Š” method ์„ธ๊ฐœ๊ฐ€ ํ•„์š”ํ•˜๋‹ค.

public protocol Subscriber : CustomCombineIdentifierConvertible {
 
  associatedtype Input
  associatedtype Failure : Error
 
  func receive(subscription: Subscription)
  func receive(_ input: Self.Input) -> Subscribers.Demand
  func receive(completion: Subscribers.Completion<Self.Failure>)
}

์‹ค์ œ Subscriber protocol์„ ๋ณด๋ฉด ๋งž๊ฒŒ ๋˜์–ด ์žˆ๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

class DecodableDataTaskSubscriber<Input: Decodable>: Subscriber {
    typealias Failure = Error // Error๊ฐ€ ๋‚˜์ง€ ์•Š๋Š”๋‹ค๋ฉด Never Type. completionํƒ€์ž…์—๋„ ๋ฐ˜์˜๋จ
 
    func receive(subscription: Subscription) { // 1. ์ฒ˜์Œ publisher์— ์˜ํ•ด ํ˜ธ์ถœ๋จ
        print("Received subscription")
        subscription.request(.unlimited) // Subscription์œผ๋กœ ๋ถ€ํ„ฐ ๋ฐ›๋Š” ๊ฐœ์ˆ˜๋ฅผ ์ •ํ• ์ˆ˜๋„ ์žˆ์Œ: subscription.request(.max(3))
    }
 
    func receive(_ input: Input) -> Subscribers.Demand { // 2. subscription์— ์˜ํ•ด ํ˜ธ์ถœ๋จ
        print("Received value: \(input)")
        return .none
    }
 
    func receive(completion: Subscribers.Completion<Error>) { // 3. ๋ชจ๋“  ๊ฐ’ ๋ฐฉ์ถœ์ด ๋๋‚˜๋ฉด ํ˜ธ์ถœ๋จ
        print("Received completion \(completion)")
    }
}
 
// ํ˜น์€ ์ด๋Ÿฐ ๋ฐฉ์‹๋„ ๊ฐ€๋Šฅํ•˜๋‹ค.
class DecodableDataTaskSubscriber<Input: Decodable, Failure: Error>: Subscriber {
 
    func receive(subscription: Subscription) {
        print("Received subscription")
        subscription.request(.unlimited)
    }
 
    func receive(_ input: Input) -> Subscribers.Demand {
        print("Received value: \(input)")
        return .none
    }
 
    func receive(completion: Subscribers.Completion<Error>) {
        print("Received completion \(completion)")
    }
}
 
class DecodableDataTaskSubscriber: Subscriber {
    typealias Input = Decodable
    typealias Failure = Error
 
    func receive(subscription: Subscription) {
        print("Received subscription")
        subscription.request(.unlimited)
    }
 
    func receive(_ input: Input) -> Subscribers.Demand {
        print("Received value: \(input)")
        return .none
    }
 
    func receive(completion: Subscribers.Completion<Error>) {
        print("Received completion \(completion)")
    }
}

์„ธ๊ฐ€์ง€ ๋ฐฉ๋ฒ•์€ ๊ฐ๊ฐ์˜ ์žฅ๋‹จ์ ์„ ๊ฐ€์ง„๋‹ค. Input, Output Type์ด ๋ณ€ํ•  ์ˆ˜ ์žˆ๋‹ค๋ฉด ๋ช…์‹œ์ ์œผ๋กœ ์ ์–ด์ฃผ๊ณ , ๊ทธ๋ ‡์ง€ ์•Š์€ ๊ฒฝ์šฐ ๋‚ด๋ถ€์ ์œผ๋กœ typealias๋ฅผ ์‚ฌ์šฉํ•ด์„œ ์ฒ˜๋ฆฌํ•ด์ฃผ์ž.

Custom Publisher

Publisher์˜ ๊ฒฝ์šฐ์—๋Š” Subscriber์—๊ฒŒ Subscription์„ ๋˜์ ธ์ฃผ๊ธฐ๋งŒ ํ•˜๋ฉด ๋œ๋‹ค. ํ•˜๋‚˜์˜ ๋ฉ”์„œ๋“œ๋งŒ ํ•„์š”ํ•  ๊ฒƒ์ด๋‹ค.

public protocol Publisher {
 
  associatedtype Output
  associatedtype Failure : Error
 
  func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input
}

์‹ค์ œ ํ”„๋กœํ† ์ฝœ๋„ ๊ทธ๋ ‡๊ฒŒ ๋˜์–ด ์žˆ๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค. ์šฐ๋ฆฌ๋Š” URLSession์—์„œ ์‚ฌ์šฉํ•  Publisher์ด๊ธฐ ๋•Œ๋ฌธ์— ๊ทธ ์•ˆ์— ๋งŒ๋“ค์–ด์„œ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ์ข‹๊ฒ ๋‹ค.

extension URLSession {
 
    func decodedDataTaskPublisher<Output: Decodable>(for urlRequest: URLRequest) -> DecodedDataTaskPublisher<Output> {
        return DecodedDataTaskPublisher<Output>(urlRequest: urlRequest)
    }
 
    struct DecodedDataTaskPublisher<Output: Decodable>: Publisher {
        typealias Failure = Error
        
        let urlRequest: URLRequest
 
        func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
            let subscription = DecodedDataTaskSubscription(urlRequest: self.urlRequest, subscriber: subscriber)
            subscriber.receive(subscription: subscription)
        }
    }
 
}

์™ธ๋ถ€์—์„œ ์‚ฌ์šฉํ•  ๋•Œ๋Š” decodedDataTaskPublisher(for urlRequest:)๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋˜๊ฒ ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ์•„์ง Subscription ๊ฐ์ฒด์— ๋Œ€ํ•ด ์ •์˜ํ•˜์ง€ ์•Š์•„ ์‚ฌ์‹ค ์ € ๋ฉ”์„œ๋“œ๋Š” ์‚ฌ์šฉ์ด ๋ถˆ๊ฐ€ํ•˜๋‹ค. ์ด ๋…€์„์„ ๋งŒ๋“ค๋Ÿฌ ๊ฐ€๋ณด์ž.

Custom Subscription

์‹ค์ œ๋กœ Subscriber์—๊ฒŒ ์š”์ฒญ์„ ๋ฐ›์•„ ๊ฐ’์„ ๋Œ๋ ค์ฃผ๋Š” ๋…€์„์ด๋‹ค. ๊ฐœ์ˆ˜์— ๋Œ€ํ•œ ์š”์ฒญ์„ ๋ฐ›์„ method๋งŒ ์ž‘์„ฑํ•ด์ฃผ๋ฉด ๋œ๋‹ค.

public protocol Subscription : Cancellable, CustomCombineIdentifierConvertible {
  func request(_ demand: Subscribers.Demand)
}

์—ฌ๊ธฐ์„œ ๋ถ€ํ„ฐ ์ž˜ ๋ด์•ผ ํ•œ๋‹ค. ์ด๋…€์„์˜ ํƒ€์ž…์„ ๋ณด๋ฉด Cancellable์„ ์ฑ„ํƒํ•˜๊ณ  ์žˆ๋‹ค. ๊ณง, ์ด๋…€์„์ด ์šฐ๋ฆฌ๊ฐ€ ๋ณดํ†ต AnyCancellable๋กœ ๋ฐ›๋Š” ๋…€์„์ด๋ผ๋Š” ๊ฑฐ๋‹ค. ๊ตฌ๋…์˜ life cycle์„ ๋‹ด๋‹นํ•˜๋Š” ์นœ๊ตฌ๊ฐ€ ์ด๋…€์„์ด๋‹ค. ์ผ๋‹จ ๋งŒ๋“ค์–ด๋ณด์ž.

extension URLSession.DecodedDataTaskPublisher {
 
    class DecodedDataTaskSubscription<Output: Decodable, S: Subscriber>: Subscription
    where S.Input == Output, S.Failure == Error {
 
        private let urlRequest: URLRequest
        private var subscriber: S?
 
        init(urlRequest: URLRequest, subscriber: S) { // ์ƒ์„ฑ ์‹œ์ ์— ์‹ค์ œ ์š”์ฒญ์„ ํ•  request ๊ฐ์ฒด์™€ subscriber๋ฅผ ๋ฐ›๋Š”๋‹ค.
            self.urlRequest = urlRequest
            self.subscriber = subscriber
        }
 
        func request(_ demand: Subscribers.Demand) { // Subscriber์ชฝ์—์„œ ์š”์ฒญํ•˜๋ฉด ์—ฌ๊ธฐ์„œ subscriber์—๊ฒŒ ์ „๋‹ฌํ•ด์ค€๋‹ค.
            if demand > 0 {
                URLSession.shared.dataTask(with: urlRequest) { [weak self] data, response, error in
                    defer { self?.cancel() }
 
                    if let data = data {
                        do {
                            let result = try JSONDecoder().decode(Output.self, from: data)
                            self?.subscriber?.receive(result)
                            self?.subscriber?.receive(completion: .finished)
                        } catch {
                            self?.subscriber?.receive(completion: .failure(error))
                        }
                    } else if let error = error {
                        self?.subscriber?.receive(completion: .failure(error))
                    }
                }.resume()
            }
        }
 
        func cancel() {
            subscriber = nil
        }
    }
}

Subscription์—์„œ subscriber๋กœ ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•ด์ค€๋‹ค. ๋‚ด๋ถ€์ ์œผ๋กœ subscriber๋ฅผ subscription์ด ๋“ค๊ณ  ์žˆ๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

Attach Whole Thing

๊ทธ๋Ÿผ ์ด์ œ ํ•œ๋ฒˆ ์‹คํ–‰ํ•ด๋ณด์ž.

struct SomeModel: Decodable {}
 
func makeTheRequest() {
    let request = URLRequest(url: URL(string: "https://www.donnywals.com")!)
    let publisher: URLSession.DecodedDataTaskPublisher<SomeModel> = URLSession.shared.decodedDataTaskPublisher(for: request)
    let subscriber = DecodableDataTaskSubscriber<SomeModel>()
    publisher.subscribe(subscriber)
}

์˜ˆ์ƒํ•˜๋Š” ๋™์ž‘์€ ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

Received subscription
Received value ~
Received completion ~

ํ•˜์ง€๋งŒ ์‹ค์ œ๋กœ ์‹คํ–‰์‹œ์ผœ๋ณด๋ฉด, subscription๋งŒ ์ถœ๋ ฅ๋œ๋‹ค.

Received subscription

๋ฌด์—‡์ด ๋ฌธ์ œ์ผ๊นŒ?

Problem

์ฐฌ์ฐฌํžˆ combine ํ˜ธ์ถœ ํ๋ฆ„์„ ๋”ฐ๋ผ๊ฐ€๋ณด๋ฉด, subscriber์— ์žˆ๋Š” method๊ฐ€ ํ˜ธ์ถœ๋˜์ง€ ์•Š์•˜๋‹ค๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค. ํ•ด๋‹น ๊ฐ์ฒด๊ฐ€ ํ• ๋‹น ํ•ด์ œ๋œ ๊ฒƒ ์ธ์ง€ ํ™•์ธํ•ด๋ณด์ž.

class DecodableDataTaskSubscriber<Input: Decodable>: Subscriber {
    typealias Failure = Error
 
    func receive(subscription: Subscription) {
        print("Received subscription")
        subscription.request(.unlimited)
    }
 
    func receive(_ input: Input) -> Subscribers.Demand {
        print("Received value: \(input)")
        return .none
    }
 
    func receive(completion: Subscribers.Completion<Error>) {
        print("Received completion \(completion)")
    }
 
    deinit {
      Swift.print("deinit subscriber")
    }
}
Received subscription
deinit subscriber
  1. subscription์€ subscriber ์ธ์Šคํ„ด์Šค๋ฅผ ์•Œ๊ณ  ์žˆ๋‹ค. (subscriber count + 1)
  2. publisher๋Š” subscriber์—๊ฒŒ subscription ๊ฐ์ฒด๋ฅผ ๋„˜๊ฒจ์ค€๋‹ค.
  3. receive(subscription: Subscription)์—์„œ subscriber๋Š” subscription์— ์š”๊ตฌ๊ฐœ์ˆ˜๋งŒํผ์„ ์š”์ฒญํ•œ๋‹ค.
  4. ํ•˜์ง€๋งŒ ํ•จ์ˆ˜ ์ธ์ž๋กœ ๋“ค์–ด์˜จ subscription ์ธ์Šคํ„ด์Šค์˜ ์ƒ๋ช…์ฃผ๊ธฐ๋Š” receive(subscription: Subscription) ๋‚ด๋ถ€์ด๋‹ค.
  5. ์Šค์ฝ”ํ”„ ์ข…๋ฃŒํ›„ subscription instance๋Š” ํ• ๋‹น ํ•ด์ œ๋œ๋‹ค.(๋ฉ”์‹œ์ง€ ๋ณด๋‚ด๊ธฐ๋„ ์ „์— ํ• ๋‹นํ•ด์ œ๋จ) ๋‚ด๋ถ€์— ์žˆ๋Š” subscriber ๊ฐ์ฒด ์—ญ์‹œ ํ• ๋‹นํ•ด์ œ ๋œ๋‹ค. (subscriber count 0)
  6. subscriber์˜ reference count๊ฐ€ 0์ด ๋˜์–ด, subscriber๋Š” ํ• ๋‹น ํ•ด์ œ๋œ๋‹ค.

์ด๋Ÿฌํ•œ ํ๋ฆ„์—์„œ ์šฐ๋ฆฌ๊ฐ€ ์•Œ ์ˆ˜ ์žˆ๋Š” ์ ์€, ๋ˆ„๊ตฐ๊ฐ€๋Š” subscription ์ธ์Šคํ„ด์Šค๋ฅผ ์†Œ์œ ํ•ด์•ผ ํ•œ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค.

Subscription Instance ์†Œ์œ ์˜ ์ฑ…์ž„

๊ทธ๋Ÿผ ์ด subscription ์ธ์Šคํ„ด์Šค๋Š” ๋ˆ„๊ฐ€ ๊ฐ€์ง€๊ณ  ์žˆ์–ด์•ผ ํ• ๊นŒ? ๋‚ด๊ฐ€ ์ฝ๊ณ  ์žˆ๋Š” ์ €์ž์˜ ๊ฒฝ์šฐ, completion์ด ํ˜ธ์ถœ๋˜๋Š” ์‹œ๊ธฐ์— subscription ๊ฐ์ฒด๋ฅผ ํ•ด์ œํ•ด์ฃผ๋Š” ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ–ˆ๋‹ค.

class DecodableDataTaskSubscriber<Input: Decodable>: Subscriber, Cancellable {
    typealias Failure = Error
    
    var subscription: Subscription?
    
    func receive(subscription: Subscription) {
        print("Received subscription")
        self.subscription = subscription
        subscription.request(.unlimited)
    }
    
    func receive(_ input: Input) -> Subscribers.Demand {
        print("Received value: \(input)")
        return .none
    }
    
    func receive(completion: Subscribers.Completion<Error>) {
        print("Received completion \(completion)")
        cancel()
    }
    
    func cancel() {
        subscription?.cancel()
        subscription = nil
    }
}

์ผ๋‹จ ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด, instance๊ฐ€ ์‚ด์•„์žˆ๊ธฐ ๋•Œ๋ฌธ์—, ๊ตฌ๋…์ด ๋ชจ๋‘ ์ข…๋ฃŒ๋˜๋ฉด subscription์„ ์ข…๋ฃŒํ•˜์—ฌ ๊น”๋”ํ•˜๊ฒŒ ํ•ด๊ฒฐ๋˜๊ธฐ๋Š” ํ•œ๋‹ค. ํ•˜์ง€๋งŒ ์ด๋ ‡๊ฒŒ ๋˜๋Š” ๊ฒฝ์šฐ, ์ด subscriber๋ฅผ ์‹ค์ œ๋กœ ์‚ฌ์šฉํ•˜๋Š” ๊ฐ์ฒด๊ฐ€ ๊ตฌ๋…์˜ ์ƒ๋ช…์ฃผ๊ธฐ๋ฅผ ๊ด€๋ฆฌํ•  ์ˆ˜๊ฐ€ ์—†๊ฒŒ ๋œ๋‹ค. ์–ด๋–ป๊ฒŒ ํ•ด์•ผํ• ๊นŒ?

์ง€๊ธˆ๊นŒ์ง€์˜ ์ฝ”๋“œ

//
//  CustomCombineViewController.swift
//  test
//
//  Created by Choiwansik on 2022/05/26.
//
 
import UIKit
import Combine
 
class DecodableDataTaskSubscriber<Input: Decodable>: Subscriber, Cancellable {
    typealias Failure = Error
 
    var subscription: Subscription?
 
    func receive(subscription: Subscription) {
        print("Received subscription")
        self.subscription = subscription
        subscription.request(.unlimited)
    }
 
    func receive(_ input: Input) -> Subscribers.Demand {
        print("Received value: \(input)")
        return .none
    }
 
    func receive(completion: Subscribers.Completion<Error>) {
        print("Received completion \(completion)")
        cancel()
    }
 
    func cancel() {
        subscription?.cancel()
        subscription = nil
    }
}
 
 
extension URLSession {
 
    func decodedDataTaskPublisher<Output: Decodable>(for urlRequest: URLRequest) -> DecodedDataTaskPublisher<Output> {
        return DecodedDataTaskPublisher<Output>(urlRequest: urlRequest)
    }
 
    struct DecodedDataTaskPublisher<Output: Decodable>: Publisher {
 
        typealias Failure = Error
 
        let urlRequest: URLRequest
 
        func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
            let subscription = DecodedDataTaskSubscription(urlRequest: self.urlRequest, subscriber: subscriber)
            subscriber.receive(subscription: subscription)
        }
    }
 
}
 
extension URLSession.DecodedDataTaskPublisher {
 
    class DecodedDataTaskSubscription<Output: Decodable, S: Subscriber>: Subscription
    where S.Input == Output, S.Failure == Error {
 
        private let urlRequest: URLRequest
        private var subscriber: S?
 
        init(urlRequest: URLRequest, subscriber: S) {
            self.urlRequest = urlRequest
            self.subscriber = subscriber
        }
 
        func request(_ demand: Subscribers.Demand) {
            if demand > 0 {
                URLSession.shared.dataTask(with: urlRequest) { [weak self] data, response, error in
                    defer { self?.cancel() }
 
                    if let data = data {
                        do {
                            let result = try JSONDecoder().decode(Output.self, from: data)
                            self?.subscriber?.receive(result)
                            self?.subscriber?.receive(completion: .finished)
                        } catch {
                            self?.subscriber?.receive(completion: .failure(error))
                        }
                    } else if let error = error {
                        self?.subscriber?.receive(completion: .failure(error))
                    }
                }.resume()
            }
        }
 
        func cancel() {
            subscriber = nil
        }
    }
}
 
struct SomeModel: Decodable {}
 
class CustomCombineViewController: UIViewController {
 
    override func viewDidLoad() {
        super.viewDidLoad()
        self.setup()
    }
 
    func makeTheRequest() {
        let request = URLRequest(url: URL(string: "https://www.donnywals.com")!)
        let publisher: URLSession.DecodedDataTaskPublisher<SomeModel> = URLSession.shared.decodedDataTaskPublisher(for: request)
        let subscriber = DecodableDataTaskSubscriber<SomeModel>()
        publisher.subscribe(subscriber)
    }
 
    func setup() {
        self.view.backgroundColor = .white
        let button = UIButton(frame: CGRect(x: 20, y: 40, width: 100, height: 50))
        button.backgroundColor = .blue
        self.view.addSubview(button)
 
        button.titleLabel?.text = "request!!"
        button.addTarget(self, action: #selector(self.buttonTapped), for: .touchUpInside)
    }
 
    @objc func buttonTapped() {
        self.makeTheRequest()
    }
 
}
 

sink์— ๋Œ€ํ•œ ์ดํ•ด

๊ตฌ๋…์˜ life cycle์„ ๊ด€๋ฆฌํ•˜๋Š” subscription ๊ฐ์ฒด๋ฅผ ๋ฆฌํ„ดํ•˜๋Š” ๊ฒƒ์ด ๋งž๋‹ค๋Š” ์ƒ๊ฐ์„ ํ–ˆ๋‹ค. ์‹ค์ œ๋กœ sink์™€ ๊ฐ™์ด apple์—์„œ ์ œ๊ณตํ•˜๋Š” ๋ฉ”์„œ๋“œ์˜ ๊ฒฝ์šฐ์— ๊ทธ๋ ‡๊ฒŒ ๊ตฌํ˜„๋˜์–ด ์žˆ์–ด, ํ•œ๋ฒˆ ์‹œ๋„ํ•ด๋ณด์•˜๋‹ค.

extension URLSession {
 
    func decodedDataTaskPublisher<Output: Decodable>(for urlRequest: URLRequest) -> DecodedDataTaskPublisher<Output> {
        return DecodedDataTaskPublisher<Output>(urlRequest: urlRequest)
    }
 
    struct DecodedDataTaskPublisher<Output: Decodable>: Publisher {
 
        // sink ๋™์ž‘์„ ๋”ฐ๋ผํ•˜์—ฌ ๋งŒ๋“ค์–ด๋ด„
        func ssink(receiveValue: (Self.Output) -> Void, receiveCompletion: (Combine.Subscribers.Completion<Self.Failure>) -> Void) -> Cancellable {
            let subscriber = DecodableDataTaskSubscriber<SomeModel>()
            let subscription = DecodedDataTaskSubscription(urlRequest: self.urlRequest, subscriber: subscriber)
            subscriber.receive(subscription: subscription)
            return subscription
        }
 
        typealias Failure = Error
 
        let urlRequest: URLRequest
 
        func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
            let subscription = DecodedDataTaskSubscription(urlRequest: self.urlRequest, subscriber: subscriber)
            subscriber.receive(subscription: subscription)
        }
    }
 
}
 
class CustomCombineViewController: UIViewController {
 
    var cancellable: Cancellable? // ์ˆ˜์ •
 
    deinit {
        self.cancellable?.cancel()
    }
 
    func makeTheRequest() {
        let request = URLRequest(url: URL(string: "https://www.donnywals.com")!)
        let publisher: URLSession.DecodedDataTaskPublisher<SomeModel> = URLSession.shared.decodedDataTaskPublisher(for: request)
        self.cancellable = publisher.ssink(receiveValue: { value in
            print("Received value: \(value)")
        }, receiveCompletion: { completion in
            print("Received completion: \(completion)")
        })
    }
}
 

์ด๋ฅผ ํ†ตํ•ด ์šฐ๋ฆฌ๊ฐ€ ๋ณดํ†ต ์‚ฌ์šฉํ•˜๋Š” sink์™€ ๊ฐ™์€ api๊ฐ€ ์–ด๋–ค ๊ตฌ์กฐ๋กœ ๋˜์–ด ์žˆ์„ ์ง€ ์œ ์ถ”ํ•ด ๋ณผ ์ˆ˜ ์žˆ๋‹ค.

  1. receiveCompletion, receiveValue์— ์›ํ•˜๋Š” ๋™์ž‘์„ ๋„ฃ๋Š”๋‹ค.
  2. ํ•ด๋‹น ์ž…๋ ฅ์„ receive(_ input: Input) -> Subscribers.Demand, receive(completion: Subscribers.Completion<Error>) method ๋‚ด๋ถ€์— ๋ฐฐ์น˜ํ•œ๋‹ค.
  3. 2์—์„œ ๋งŒ๋“ค์–ด์ง„ subscriber ์ธ์Šคํ„ด์Šค๋ฅผ Publisher์— ์žˆ๋Š” receive<S>(subscriber: S) ์•ˆ์— ๋„ฃ์–ด ํ˜ธ์ถœํ•œ๋‹ค.
  4. publisher๋Š” Subscriber์˜ receive(subscription: Subscription)๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค.
  5. subscriber๋Š” Subscription์˜ request(_ demand: Subscribers.Demand)๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค.
  6. Subscription์€ Subscriber์˜ receive(_ input: Input) -> Subscribers.Demand๋ฅผ ์š”๊ตฌ ๊ฐœ์ˆ˜์— ๋งž๊ฒŒ ํ˜ธ์ถœํ•ด์ค€๋‹ค.
  7. Subscription์ด ๋๋‚˜๋ฉด subscriber์˜ receive(completion: Subscribers.Completion<Error>)๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค.
  8. ์œ„์˜ ์ž‘์—…์„ ์„ธํŒ…ํ•œ๋‹ค. (์•„์ง ์‹ค์ œ๋กœ ์‹คํ–‰๋˜์ง€ ์•Š์Œ)
  9. ์ด ์ž‘์—…์˜ ์ƒ๋ช…์ฃผ๊ธฐ๋ฅผ ๊ด€์žฅํ•˜๋Š” Subscription์„ instance๋กœ ๋ฆฌํ„ดํ•œ๋‹ค.
  10. ์‚ฌ์šฉํ•˜๋Š” ์ชฝ์—์„œ๋Š” ์ด Subscription์„ ๋ณ€์ˆ˜๋กœ ๋ฐ›๋Š”๋‹ค.

๋ณ€์ˆ˜๋กœ ๋ฐ›์ง€ ์•Š์•˜์„ ๋•Œ์˜ ์ƒํ™ฉ์„ ์ƒ๊ฐํ•ด๋ณด์ž. ๋งŒ์•ฝ ๋ฐ›์ง€ ์•Š๋Š”๋‹ค๋ฉด Subscription์˜ reference count๊ฐ€ 0์ด ๋œ๋‹ค. Subscription์—์„œ๋Š” ๋ณดํ†ต Cancellable์„ ์ฑ„ํƒํ•˜๊ฒŒ ๋˜๋ฏ€๋กœ, reference count๊ฐ€ 0์ด ๋˜๋Š” ์‹œ์ ์— cancel() method๊ฐ€ ํ˜ธ์ถœ๋˜๊ฒŒ ๋œ๋‹ค. ์ด cancel() method์—๋Š” ๊ฐ€์ง€๊ณ  ์žˆ๋Š” subscriber instance๋ฅผ ํ• ๋‹นํ•ด์ œํ•˜๋Š” ๋กœ์ง์ด ์ฒจ๋ถ€๋˜์–ด ์žˆ๋‹ค. ์ด๋ง์€ ์ฆ‰์Šจ, ๋ณ€์ˆ˜๋กœ ๋ฐ›์ง€ ์•Š์•˜์„ ๊ฒฝ์šฐ subscription์ด ์˜ค์ง€ ์•Š๋Š”๋‹ค๋Š” ๋ง๊ณผ ๋™์‹œ์—, subscriber instance์˜ ๋ฉ”๋ชจ๋ฆฌ ํ•ด์ œ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ชฝ์—์„œ ๊ด€๋ฆฌํ•  ํ•„์š”๊ฐ€ ์—†๋‹ค๋Š” ๋ง์ด๋‹ค.

์—ฌ๊ธฐ์„œ ์˜๋ฌธ ์‚ฌํ•ญ์ด ์žˆ์„ ํ…๋ฐ, Cancellable๋กœ ๊ตณ์ด ๋ฆฌํ„ดํ•˜๋Š” ์ด์œ ๊ฐ€ ๋ฌด์—‡์ธ์ง€? AnyCancellable๋กœ ํƒ€์ž… erasing์„ ํ•˜๋Š”๊ฒŒ ์ข‹์€ ๊ฒƒ ์•„๋‹Œ์ง€? ์™€ ๊ฐ™์€ ์˜๋ฌธ์ด ๋“ค ์ˆ˜ ์žˆ๋‹ค. ์‹ค์ œ๋กœ ์ ์šฉํ•ด๋ณด๋‹ˆ AnyCancellable์€ ๋‹จ์ˆœํžˆ Cancellable์˜ Type erasing์„ ์œ„ํ•œ ๊ฒƒ์ด ์•„๋‹Œ ๋“ฏํ•˜๋‹ค. ๊ทธ๋Ÿฐ ๋ฉ”์„œ๋“œ๋„ ์—†์—ˆ๊ณ , Anycancellable์˜ ๊ฒฝ์šฐ class์˜€๋‹ค. ๊ทธ๋ž˜์„œ ์ด๋Ÿฌํ•œ ์ ์— ๋Œ€ํ•ด ๋‹ค์‹œ ๊ณต๋ถ€๋ฅผ ํ•ด์•ผ ํ•  ๊ฒƒ ๊ฐ™์•„ ์ผ๋‹จ์€ ๊ธ€์„ ์—ฌ๊ธฐ์„œ ๋ฉˆ์ถ˜๋‹ค.

์ž ๊น ์กฐ์‚ฌํ•ด๋ณธ ๊ฒฐ๊ณผ๋กœ๋Š” AnyCancellable์ธ ๊ฒฝ์šฐ์—๋Š” ๋ฉ”๋ชจ๋ฆฌ์—์„œ ํ•ด์ œ๋˜๋Š” ์‹œ์ ์— ์ž๋™์œผ๋กœ cancel()์„ ํ˜ธ์ถœํ•ด์ค€๋‹ค๊ณ  ํ•œ๋‹ค. ์ง€๊ธˆ์€ cancellable์ด๋ผ ๋ช…์‹œ์ ์œผ๋กœ ์ ์–ด๋‘์—ˆ๋‹ค.

Whole Code

//
//  CustomCombineViewController.swift
//  test
//
//  Created by Choiwansik on 2022/05/26.
//
 
import UIKit
import Combine
 
class DecodableDataTaskSubscriber<Input: Decodable>: Subscriber {
    typealias Failure = Error
 
    var subscription: Subscription?
 
    func receive(subscription: Subscription) {
        print("Received subscription")
        self.subscription = subscription
        subscription.request(.unlimited)
    }
 
    func receive(_ input: Input) -> Subscribers.Demand {
        print("Received value: \(input)")
        return .none
    }
 
    func receive(completion: Subscribers.Completion<Error>) {
        print("Received completion \(completion)")
    }
}
 
 
extension URLSession {
 
    func decodedDataTaskPublisher<Output: Decodable>(for urlRequest: URLRequest) -> DecodedDataTaskPublisher<Output> {
        return DecodedDataTaskPublisher<Output>(urlRequest: urlRequest)
    }
 
    struct DecodedDataTaskPublisher<Output: Decodable>: Publisher {
 
        // sink ๋™์ž‘์„ ๋”ฐ๋ผํ•˜์—ฌ ๋งŒ๋“ค์–ด๋ด„
        func ssink(receiveValue: (Self.Output) -> Void, receiveCompletion: (Combine.Subscribers.Completion<Self.Failure>) -> Void) -> Cancellable {
            let subscriber = DecodableDataTaskSubscriber<SomeModel>()
            let subscription = DecodedDataTaskSubscription(urlRequest: self.urlRequest, subscriber: subscriber)
            subscriber.receive(subscription: subscription)
            return subscription
        }
 
        typealias Failure = Error
 
        let urlRequest: URLRequest
 
        func receive<S>(subscriber: S) where S : Subscriber, Failure == S.Failure, Output == S.Input {
            let subscription = DecodedDataTaskSubscription(urlRequest: self.urlRequest, subscriber: subscriber)
            subscriber.receive(subscription: subscription)
        }
    }
 
}
 
extension URLSession.DecodedDataTaskPublisher {
 
    class DecodedDataTaskSubscription<Output: Decodable, S: Subscriber>: Subscription
    where S.Input == Output, S.Failure == Error {
 
        private let urlRequest: URLRequest
        private var subscriber: S?
 
        init(urlRequest: URLRequest, subscriber: S) {
            self.urlRequest = urlRequest
            self.subscriber = subscriber
        }
 
        func request(_ demand: Subscribers.Demand) {
            if demand > 0 {
                URLSession.shared.dataTask(with: urlRequest) { [weak self] data, response, error in
                    defer { self?.cancel() }
 
                    if let data = data {
                        do {
                            let result = try JSONDecoder().decode(Output.self, from: data)
                            self?.subscriber?.receive(result)
                            self?.subscriber?.receive(completion: .finished)
                        } catch {
                            self?.subscriber?.receive(completion: .failure(error))
                        }
                    } else if let error = error {
                        self?.subscriber?.receive(completion: .failure(error))
                    }
                }.resume()
            }
        }
 
        func cancel() {
            subscriber = nil
        }
    }
}
 
struct SomeModel: Decodable {}
 
class CustomCombineViewController: UIViewController {
 
    var cancellable: Cancellable? // ์ˆ˜์ •
 
    override func viewDidLoad() {
        super.viewDidLoad()
        self.setup()
    }
 
    deinit {
        self.cancellable?.cancel()
    }
 
    func makeTheRequest() {
        let request = URLRequest(url: URL(string: "https://www.donnywals.com")!)
        let publisher: URLSession.DecodedDataTaskPublisher<SomeModel> = URLSession.shared.decodedDataTaskPublisher(for: request)
        self.cancellable = publisher.ssink(receiveValue: { value in
            print("Received value: \(value)")
        }, receiveCompletion: { completion in
            print("Received completion: \(completion)")
        })
    }
 
    func setup() {
        self.view.backgroundColor = .white
        let button = UIButton(frame: CGRect(x: 20, y: 40, width: 100, height: 50))
        button.backgroundColor = .blue
        self.view.addSubview(button)
 
        button.titleLabel?.text = "request!!"
        button.addTarget(self, action: #selector(self.buttonTapped), for: .touchUpInside)
    }
 
    @objc func buttonTapped() {
        self.makeTheRequest()
    }
 
}
 

์ •๋ฆฌ

  • Subscriber๋Š” ๊ถ๊ทน์ ์œผ๋กœ Subscription์—๊ฒŒ ๊ฐ’์„ ๋ฐ›๋Š”๋‹ค.
  • Subscription์€ life cycle์„ ๊ด€๋ฆฌํ•œ๋‹ค.
  • Subscription์€ Subscriber๋ฅผ ๋“ค๊ณ  ์žˆ์„ ์ˆ˜ ๋ฐ–์— ์—†์œผ๋ฉฐ, ์ฑ„ํƒํ•œ Protocol Subscription์ด Cancellable์„ ์ด๋ฏธ ์ฑ„ํƒํ•˜๊ณ  ์žˆ๋‹ค.
  • subscriber์—์„œ subscription์„ ์•Œ๊ณ ์žˆ๋‹ค๊ฐ€ ํŠน์ • ์‹œ์ ์— ํ•ด์ œํ•ด์ฃผ๋“ , ์•„๋‹ˆ๋ฉด ๋ฐ–์œผ๋กœ ๋นผ๋“ ํ•˜์—ฌ ๊ตฌ๋…์˜ life cycle์„ ํ•ด์ œํ•ด์ฃผ์–ด์•ผ ํ•œ๋‹ค.
  • sink๋Š” sugar api๋กœ ์ด ๊ตฌ๋…๊ถŒ์„ ๋ฐ–์œผ๋กœ ๋นผ์ฃผ๋Š” ๋“ฏํ•˜๋‹ค.

๋‹ค์Œ์—๋Š” AnyCancellableํ•˜๊ณ  Cancellable์˜ ์ฐจ์ด๋ฅผ ์ข€ ๋ด์•ผํ•  ๋“ฏ ํ•˜๋‹ค. ๋!

Reference