iOS/Combine

[Combine] Publisher & Subscriber - Combine 공부 2

Dev_Pingu 2021. 12. 31. 00:13
반응형

안녕하세요 Pingu입니다.🐧

 

지난 글에서는 간단하게 Combine이 뭔지에 대해 알아봤습니다.

간단히 다시 요약해보면 Apple에서 2019년에 만든 새로운 프레임워크인데, 이걸 쓰면 비동기 이벤트를 간단하게 처리할 수 있다! 정도?입니다. (Apple에서 만든 RxSwift라고 봐도 됩니다.😄)

 

어쨌든 Combine에는 Publisher, Subscriber, Subscription, Operator가 있는데, Operator는 종류가 너무 많으니 이번 글에서는 Publisher, Subscriber, Subscription에 대해 알아보려고 합니다.

 

흐름

공부를 하다보니 일단 Publisher, Subscriber, Subscription이 뭔지 알기 전에 이것들이 어떻게 동작되는지 흐름을 알고 가면 좋을 거 같더라고요.

그래서 위와 같이 간단하게 흐름도를 만들어 봤습니다.

간단한 예제도 한 번 볼게요.

class IntSubscriber: Subscriber {
    typealias Input = Int
    typealias Failure = Never
    
    // Publisher가 Subscription주면 호출됨
    func receive(subscription: Subscription) {
        subscription.request(.max(1))
    }
    
    // Publisher가 주는 값을 처리
    func receive(_ input: Int) -> Subscribers.Demand {
        print("Received value: \(input)")
        // Publisher에게 한 번 더 달라고 요청
        return .max(1)
    }
    
    func receive(completion: Subscribers.Completion<Never>) {
        print("Received completion: \(completion)")
    }
}

// IntArray를 하나 만듭니다.
let intArray: [Int] = [1,2,3,4,5]

// IntSubscriber를 만듭니다.
let intSubscriber = IntSubscriber()

// IntArray Publisher를 subscribe 합니다.
// intSubscriber가 intArray.publisher에게 값을 요청하면 달라고 말합니다.
intArray.publisher
    .subscribe(intSubscriber)

위와 같이 간단하게 예제를 만들어봤는데요, 아까 본 흐름도 그림으로 이해해보면

  1. intArray를 하나 만들어서 그냥 존재하게 만들었습니다.
  2. intSubscriber도 하나 만듭니다.
  3. intSubscriber가 intArray.publisher에게 값을 요청하면 달라고 말합니다. subscribe(_:) 메서드 부분이에요!
  4. 그럼 Publisher가 subscription을 만들어서 receive(subscription:)을 통해 Subscriber에게 줍니다.
  5. Subscriber는 Subscription을 받으면 Publisher에게 request(_:)를 통해 값을 달라고 합니다.
  6. Publisher는 값을 Subscriber에게 주다가 더 이상 줄 게 없으면 completion event를 전달합니다.

실행해보면 결과는 아래와 같이 나옵니다!

Received value: 1
Received value: 2
Received value: 3
Received value: 4
Received value: 5
Received completion: finished

흠.. 흐름이 이해가 되시나요?

여러 용어가 나오면서 헷갈리실 거 같은데.. 그래도 흐름을 이해하시는데 도움이 되셨길 바라면서 하나씩 자세히 알아보도록 하겠습니다.

Publisher

Combine의 핵심 역할을 하는 Publisher는 Protocol입니다.

근데 공부하다 보니 Publishers도 있더라고요? 헷갈리게..;

정의를 보면 Publishers는 Operator들을 모아둔 Enum입니다.

이후에 알아볼 여러 가지 Operator들이 여기에 정의되어있습니다.

 

어쨌든 다시 Publisher로 돌아가서, Publisher 프로토콜은 아래와 같이 생겼습니다.

만약에 여러분이 직접 Publisher를 구현하고자 한다면 Output, Failure, receive(subscriber:)는 반드시 구현해야 합니다.

얘네가 하는 역할을 알아보면 다음과 같아요.

  • Output
    • Publisher가 생성할 수 있는 값의 타입을 나타냅니다.
  • Failure
    • Publisher가 생성할 수 있는 Error 타입을 나타냅니다.
    • 만약 Error를 생성하지 않는 Publisher를 만들고 싶다면 Never 타입으로 설정해주면 됩니다!
  • receive(subscriber:)
    • Publisher 자신을 subscribe 하는 subscriber를 받습니다.

음 근데 아까 흐름 설명할 때 못 봤던 receive(subscriber:)라는 메서드가 있네요.

아까 예제에서는 subscribe(_:)였던 거 같은데 말이죠.

subscribe(_:)는 이렇게 선언되어있습니다.

설명을 보면 여기서 receive(subscriber:) 대신에 얘를 호출하라고 합니다.

그러니까 Subscriber가 Publisher의 subscribe메서드를 호출하면 자신을 receive(subscriber:) 메서드를 통해 Publisher에게 알리게 되는 거죠.

 

말을 적다 보니 좀 복잡하네요🥲

 

Publisher를 한 마디로 정리해보자면 "자신을 Subscribe 한 Subscriber에게 값을 내보내는 프로토콜" 정도가 되겠네요.

 

Publisher는 프로토콜이라고 했었는데요, 그래서 애플은 자주 쓸 것 같은 Publisher들을 미리 정의해뒀는데요 걔네들은 아래와 같은 이름을 가지고 있습니다.

  • Deferred
  • Empty
  • Fail
  • Future
  • Just
  • Record

이것들에 대해서는 다음 글에서 자세히 알아보도록 하겠습니다.

 

이 외에도 기존의 명령형 코드(Combine은 선언적 코드)에서 Combine Subscriber에게 값을 보내는 중개자 역할을 수행하는 Subject도 있는데요, 이런 것들은 다음 글에서 알아보도록 하겠습니다.

 

그럼 다음으로는 Subscriber를 알아보죠~!

Subscriber

Subscriber도 역시 Protocol입니다.

아까 위에서 제가 구현한 간단한 예제에서 IntSubscriber를 만들었었는데, 그때처럼 직접 구현하려고 한다면 아래의 것들은 필수적으로 구현해야 합니다.

  • Input
    • Publisher에게 받는 값의 타입입니다.
  • Failure
    • Publisher에게 받는 Error 타입입니다.
    • 만약 Error를 수신하지 않고 싶다면 Never 타입으로 설정해주면 됩니다!
  • receive(subscription:)
    • Publisher가 만들어서 주는 subscription을 받습니다.
  • receive(input:)
    • Publisher가 주는 값을 받습니다.
    • Demand를 반환하는데 이는 값을 더 원하는지에 대한 여부입니다.
  • receive(completion:)
    • Publisher가 주는 completion event를 받습니다.

아까 Publisher에서는 값의 타입 이름이 Output이었고 Subscriber에서는 Input이네요.

Combine을 사용할 때 주의할 점은 Publisher의 <Output, Failure> 타입과 Subscriber의 <Input, Failure> 타입이 동일해야 한다는 것!입니다.

이게 다르면 Publisher와 Subscriber는 서로 값을 주고받지 못합니다.

 

그리고 receive(input:)의 반환 타입이 Subscribers.Demand인 거 보이시나요?

Publishers에 이어서 또 헷갈리게 Subscribers도 있네요..ㅡㅡ

얘는 또 뭘까요?

정의를 보니 Subscriber 역할을 하는 타입들을 정의해둔 곳이라고 하네요.

실제로 뭐가 있는지 보면 아래와 같습니다.

  • Demand
  • Completion
  • Sink
  • Assign

이것들도 다음 글에서 자세히 알아보도록 하고 지금은 Subscriber 프로토콜에 있던 Demand만 간단히 살펴보고 넘어가겠습니다.

정의를 보면 Subscription을 통해 Subscriber가 Publisher에게 보낸 request 횟수라고 하네요.

즉 Subscriber가 Publisher에게 값을 달라고 요청할 수 있는데 그런 요청을 한 횟수입니다.

즉 아까 receive(input:)의 반환 타입이 Demand인데요, 이는 Publisher에게 몇 번 더 값을 달라고 요청할지에 대한 값입니다.

 

아까 간단히 구현한 IntSubscriber의 receive(input:)을 다시 볼게요.

// Publisher가 주는 값을 처리
func receive(_ input: Int) -> Subscribers.Demand {
	print("Received value: \(input)")
	// Publisher에게 한 번 더 달라고 요청
	return .max(1)
    
	// Publisher에게 값 더 이상 안줘도 된다고 알림
	// return .none
	// Publisher에게 끝없이 값을 달라고 요청
	// return .unlimited
}

위와 같이. max(1)을 반환하면 한 번 더 달라고 요청하는 것입니다.

그 외에도. none,. unlimited가 있는데요 이에 대한 설명은 주석으로 달아뒀으니 참고해주세요~

Subscription

그럼 마지막으로 Publisher가 만들어서 Subscriber에게 준다고 했던 Subscription에 대해서 알아보겠습니다.

이 Subscription 역시 Protocol입니다.

공식문서에서 정의를 보면...

"Publisher와 Subscriber를 연결하는 프로토콜"이라고 합니다.

실제로 어떤 걸 요구하는지 보면 아래와 같아요.

Publisher나 Subscriber에 비해 아주 간단하네요.

딱 request(demand:)만 필요로 합니다.

 

이걸 어디서 썼었는지 기억하시나요?

아까 예제에서 Subscriber의 receive(subscription:) 메서드를 다시 볼게요.

// Publisher가 Subscription주면 호출됨
func receive(subscription: Subscription) {
    // Subscription에게 값을 1번 요청
    subscription.request(.max(1))
}

이렇게 사용했었네요!

Publisher에게 받은 Subscription의 request(demand:)를 호출해서 Publisher에게 값을 요청합니다.

Demand는 아까 말한 대로 Publisher에게 값을 몇 번 달라고 요청하는 것입니다.

 

즉 Subscriber가 Publisher에게 값을 요청할 때 Subscription을 사용한다고 볼 수 있습니다.

이렇게 이해하면 Subscription의 정의가 잘 이해가 되는 듯하네요.

 

또한 공식문서를 보면, Subscription은 특정 Subscriber가 Publisher에 연결된 순간 설정되는 Identity가 있어서 한 번만 cancel 할 수 있고 cancel 하면 이전에 연결된 Subscriber들이 모두 해제합니다. 이런 과정은 스레드로부터 안전해야 합니다.

 

cancel을 할 수 있는 이유는 Subscription 프로토콜이 Cancellable 프로토콜을 채택했기 때문입니다.

실제로 Cancellable 프로토콜을 보면 아래와 같이 생겼습니다.

public protocol Cancellable {
    func cancel()
}

이렇게 Subscription에 명시적으로 cancel()을 호출하지 않으면 Publisher가 complete 될 때까지나 메모리에서 subscription이 해제될 때까지 Subscription이 계속됩니다.

 

애플에서는 Cancellable을 채택한 AnyCancellable 클래스도 구현해뒀습니다.

final public class AnyCancellable: Cancellable, Hashable {
    public init(_ cancel: @escaping () -> Void)
    
    public init<C>(_ canceller: C) Where C: Cancellable
    
    final public func cancel()
}

 

이렇게 AnyCancellable은 cancel()이 호출되면 실행될 closure를 설정할 수도 있습니다.

 

Subscriber를 구현할 때 Anycancellable을 사용해서 publisher를 cancel 할 수 있지만 Subscription 객체를 사용해서 item을 요청할 수 없는 cancellation token을 제공할 수도 있다고 합니다.

 

그리고 AnyCancellable은 메모리에서 해제될 때 자동으로 cancel()을 호출합니다.

 

실제로 간단히 사용해보면 이해가 빠릅니다.

// AnyCancellable
let subject = PassthroughSubject<Int, Never>()
let anyCancellable = subject
    .sink(receiveCompletion: { completion in
        print("received completion: \(completion)")
    }, receiveValue: { value in
        print("received value: \(value)")
    })

subject.send(1)
anyCancellable.cancel()
subject.send(2)

이렇게 하면 1은 전달되지만 2는 이미 cancel()이 호출된 후라서 전달되지 않습니다.

 

 근데 이렇게 cancel()할 때 실행될 closure도 구현할 수 있다고 했는데요 이건 아래와 같이 사용할 수 있습니다.

let subject = PassthroughSubject<Int, Never>()
let anyCancellable = subject
    .handleEvents(
        receiveCancel: {
            // cancel()이 불렸을 때 호출될 클로저
            print("Cancel 불렸음!")
        })
    .sink(receiveValue: { value  in
        print("Receive Value: \(value)")
    })
subject.send(1)
anyCancellable.cancel()

handleEvents는 여러 가지 이벤트가 발생했을 때 처리할 수 있는 Operator입니다. 여러가지 이벤트를 처리할 수 있지만 위 예제에서는 cancel이 호출됐을 경우에만 뭔갈 하도록 구현했어요.

 

실제로 실행해보면 아래와 같은 결과를 볼 수 있습니다.

Receive Value: 1
Cancel 불렸음!

이후에 알아보겠지만 sink는 Subscriber를 만들고 바로 request 하는 operator입니다.

 

이렇게 Combine에서 가장 중요한 Publisher, Subscriber, Subscription에 대해서 알아봤습니다.

 

마지막으로 정리하면

  • Publisher는 값이나 completion event를 Subscriber에게 전달합니다.
  • Subsriber는 Subscription을 통해 Publisher에게 값을 요청합니다.
  • Subscription은 Publisher와 Subscriber 사이를 연결합니다.
  • Subscription은 cancel()을 통해 취소할 수 있으며 이때 호출될 클로저를 설정할 수 있습니다.

정도가 되겠네요.

 

최대한 이해가 쉽게 되도록 작성해봤는데... Combine을 공부하시는 누군가에게 도움이 되셨으면 좋겠네요.

다음 글에서는 애플에서 3개의 프로토콜을 사용해서 미리 구현해둔 클래스나 구조체를 살펴볼 거 같아요.

 

감사합니다~!

반응형