iOS/Combine

[Combine] Subscription - Combine 공부 6

Dev_Pingu 2022. 1. 24. 01:01
반응형

안녕하세요 Pingu입니다.🐧

 

지난 글에서는 Apple에서 Subscriber 사용을 위해 미리 정의해둔 것들에 대해 알아봤었는데요, 이번 글에서는 이전에 배운 Publisher, Subscriber를 연결하는 역할을 하는 Subscription에 대해 알아보려고 합니다.

 

Subscription이란?

Subscription은 정의부터 어떤 녀석인지 느낌이 옵니다.

Subscription도 프로토콜이며 Cancellable이라는 걸 채택했네요.

정의를 보면 Subscriber와 Publisher의 연결을 나타내는 프로토콜이라고 합니다.

 

설명을 좀 더 보면 Subscription에는 특정 Subscriber가 Publisher를 subscribe 할 때 정의되는 ID가 있어서 Class로만 정의해야 한다고 합니다. 또한 Subscription을 cancel 하는 작업은 스레드로부터 안전해야 한다고 하며 cancel은 한 번만 할 수 있다고 해요. Subscription을 cancel 하면 Subscriber를 연결해서 할당된 모든 리소스도 해제된다고 합니다.

 

구현을 살펴보면 다음과 같습니다.

@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public protocol Subscription : Cancellable, CustomCombineIdentifierConvertible {
    func request(_ demand: Subscribers.Demand)
}
  • request(_ demand: Subscribers.Demand)
    • Publisher에게 Subscriber가 값을 요구하는 횟수를 알려줍니다.

정의는 되게 간단합니다. Publisher와 Subscriber를 연결하는 역할을 하다보니 그 둘 사이의 소통을 담당하는 request 메서드만 있네요.

 

Subscription 프로토콜이 채택하고 있던 다른 프로토콜들인 Cancellable, CustomCombineIdentifierConvertible도 간단히 살펴보겠습니다.

Cancellable

Cancellable의 정의를 보면 다음과 같습니다.

그냥 간단하게 cancel을 할 수 있는 녀석을 나타내는 프로토콜이라고 합니다.

구현을 살펴봐도 간단합니다.

@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public protocol Cancellable {
    func cancel()
}

그냥 간단하게 cancel() 메서드만 있네요.

 

Custom Publisher를 구현하기 위해 Cancellable을 만들 때 cancel() 메서드는 Publisher가 downstream의 Subscriber request를 중지하는 작업을 수행하면 된다고 합니다. Combine은 Publisher가 즉시 멈추는 것을 요구하지는 않지만 cancel() 메서드의 호출은 빠르게 적용된다고 하네요. 그리고 아까 언급한 대로 cancel()은 첫 호출 때만 작동하고 그 이후에는 작동되면 안 된다고 합니다.

 

Cancellable의 extension 구현에 자주 보던게 있어서 그거도 가지고 와 봤습니다.

@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
extension Cancellable {
    @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
    public func store<C>(in collection: inout C) where C : RangeReplaceableCollection, C.Element == AnyCancellable

    @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
    public func store(in set: inout Set<AnyCancellable>)
}

얘는 Subscription을 저장할 때 사용하는 메서드입니다. Subscription을 만들고 어딘가에 저장해놓지 않으면 메모리에서 할당 해제되어 동작되지 않는데, store() 메서드는 이를 방지하기 위해 Subscription 저장을 할 때 자주 사용했었어요.

 

간단하게 사용해보면 다음과 같습니다!

var subscriptions = Set<AnyCancellable>()
let stringArray: [String] = ["Pingu", "Pinga"]
stringArray.publisher
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)

위 코드와 같이 사용하면 subscriptions에 subscription이 저장되게 됩니다. 근데 왜 타입이 AnyCancellable이냐?면

이렇게 예전에 알아본 sink가 반환하는 타입이 AnyCancellable이기 때문입니다.

AnyCancellable

그럼 AnyCancellable이 뭔지 정의를 보면 다음과 같습니다.

Cancellable 객체의 타입의 타입을 지운 것이라고 합니다.

 

이전 글에서 알아본 AnySubscriber, AnyPublisher와 비슷하게 모든 Cancellable도 AnyCancellable로 쉽게 처리할 수 있습니다.

Custom Combine Identifier Convertible

얘도 Subscription이 채택한 프로토콜이었는데, 정의를 보면 다음과 같습니다.

이름에서 알 수 있듯이 Publisher Stream을 식별하기 위한 프로토콜이라고 합니다.

 

여러 개의 Subscription들이 연결된 Publisher 체인에서 이 프로토콜을 사용해서 Subscription을 식별할 수 있다고 하네요.

 

구현도 간단합니다.

@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public protocol CustomCombineIdentifierConvertible {
    var combineIdentifier: CombineIdentifier { get }
}

그냥 뭔가 Hashable을 채택했을 것만 같은 프로퍼티 하나가 있네요. 저거로 구분하나 봅니다.

 

그럼 이제 Subscription을 한 번 직접 사용해보겠습니다.

 

일단 Publisher의 Input, Subscriber의 Output으로 사용할 객체를 하나 만들어줍니다.

struct YoutubeSubscriber {
    let name: String
    let age: Int
}

그리고 이 객체를 사용할 Subscription과 Publisher를 만들어줍니다.

// Subscription
final class PinguSubscription<S: Subscriber>: Subscription where S.Input == YoutubeSubscriber {
    var requested: Subscribers.Demand = .none
    var youtubeSubscribers: [YoutubeSubscriber]
    var subscriber: S?
    
    init(subscriber: S,
         youtubeSubscribers: [YoutubeSubscriber]) {
        print("PinguSubscription이 생성됨!")
        self.subscriber = subscriber
        self.youtubeSubscribers = youtubeSubscribers
    }
    
    func request(_ demand: Subscribers.Demand) {
        print("요청받은 demand : \(demand)")
        for youtubeSubscriber in youtubeSubscribers {
            subscriber?.receive(youtubeSubscriber)
        }
        
//        subscriber?.receive(completion: .finished)
    }
    
    func cancel() {
        print("PinguSubscription이 cancel됨!")
        youtubeSubscribers.removeAll()
        subscriber = nil
    }
}
// Publisher
extension Publishers {
    struct PinguPublisher: Publisher {
        var youtubeSubscribers: [YoutubeSubscriber]
        
        func receive<S>(subscriber: S)
        where S : Subscriber, Never == S.Failure, YoutubeSubscriber == S.Input {
            let subscription = PinguSubscription(subscriber: subscriber, youtubeSubscribers: youtubeSubscribers)
            subscriber.receive(subscription: subscription)
        }
        
        typealias Output = YoutubeSubscriber
        typealias Failure = Never
        
        mutating func append(subscriber: YoutubeSubscriber) {
            youtubeSubscribers.append(subscriber)
        }
    }
    
    static func pingu(youtubeSubscribers: [YoutubeSubscriber]) -> Publishers.PinguPublisher {
        return Publishers.PinguPublisher(youtubeSubscribers: youtubeSubscribers)
    }
}

 

이렇게 만들어주면 준비는 끝!입니다.

PinguSubscription 코드를 조금 살펴보면, request(), cancel()을 직접 구현해놓은 것을 볼 수 있습니다. 그리고 cancel()이 호출되는 것을 볼 수 있도록 finished 전달 코드는 주석 처리해뒀습니다.

 

그리고 그렇게 만든 PinguSubscription을 PinguPublisher의 receive(subsriber:)에서 subscriber에게 전달해줍니다.

 

이렇게 구현한 것들을 사용해보면..

var subscriptions = Set<AnyCancellable>()
let youtubeSubscriber1 = YoutubeSubscriber(name: "Pingu", age: 7)
let youtubeSubscriber2 = YoutubeSubscriber(name: "Pinga", age: 5)

var youtubeSubscribers = [youtubeSubscriber1, youtubeSubscriber2]

let pinguPublisher = Publishers.pingu(youtubeSubscribers: youtubeSubscribers)
pinguPublisher
    .sink(receiveCompletion: { print($0) },
          receiveValue: { print("name: \($0.name), age: \($0.age)") }
    )
    .store(in: &subscriptions)

for subscription in subscriptions {
    subscription.cancel()
}

위와 같이 사용할 수 있고 실행해보면 결과는 다음과 같이 나옵니다.

PinguSubscription이 생성됨!
요청받은 demand : unlimited
name: Pingu, age: 7
name: Pinga, age: 5
PinguSubscription이 cancel됨!

이렇게 Subscription의 메서드들을 직접 구현하고 호출되는 것을 확인할 수 있습니다. 어떻게 응용하느냐에 따라 정말 다양하게 사용할 수 있을 거 같아요.

 

다음 글 부터는 여러가지 Operator에 대해 하나씩 알아보도록 하겠습니다.

 

전체 코드는 여기서 볼 수 있습니다!

 

감사합니다.

반응형