felix-iOS

[번역글] Combine Tutorial: Getting Started(2) Subject, Subscription 본문

Combine

[번역글] Combine Tutorial: Getting Started(2) Subject, Subscription

felix-mr 2021. 5. 27. 15:27

지난 첫 게시글에 이어서 두번째 게시글을 작성해보려고 합니다 :)

이건 지난 글이고

https://felix-mr.tistory.com/2

 

[번역글] Combine Tutorial: Getting Started(1) Publisher, Subscriber

안녕하세요 :) 블로그 첫 글을 쓰게 되었습니다! 요즘 Combine + UIKit 으로 프로젝트를 진행하고 있어서 Combine 관련된 글을 써보려고 합니다. 저도 학습하는 입장이기 때문에 번역에 대한 제 사견에

felix-mr.tistory.com

이건 원글입니다👍

https://www.vadimbulavin.com/swift-combine-framework-tutorial-getting-started/

 

Swift Combine Framework Tutorial: Getting Started

Get started with the Swift Combine framework in this tutorial. Let's study what are Combine publisher, subscriber, operators, subject and publisher-subscriber life cycle; and implement several examples with the Combine framework in Swift 5.

www.vadimbulavin.com

 

저번 게시글에서는 Combine의 Publisher와 Subscriber에 대해서 알아보았습니다. 이번 게시글에서는 Subject와 Subscription 그리고 Subscription의 LifeCycle에 대해서 알아보겠습니다. 이전 게시글을 확인하지 않으신 분은 한 번 보고 오시면 더 이해가 쉬우실 것 같습니다 :)

 

틀린 부분에 대해서는 댓글 부탁드려유👮‍♀️

 

Subject

Subject는 외부에서 전달된 값을 스트림에 삽입(insert)할 수 있는 특별한 종류의 Publisher입니다. Subject protocol은 값을 전달하는 3가지의 방법을 제공합니다.

public protocol Subject : AnyObject, Publisher {
  func send(_ value: Self.Output)
  func send(completion: Subscribers.Completion<Self.Failure>)
  func send(subscription: Subscription)
}

Combine에서는 기본적으로 PassthroughSubjectCurrentValueSubject를 제공합니다. 먼저 PassthroughSubject에 대해 직접 사용해보면서 알아보겠습니다.

// 1
let subject = PassthroughSubject<String, Never>()

// 2
subject.sink(receiveCompletion: { _ in
  print("finished")
}, receiveValue: { value in
  print(value)
})

// 3
subject.send("Hello")
subject.send("World")

// 4
subject.send(completion: .finished)
  1. PassthroughSubject를 생성합니다. Subject 또한 Publisher의 종류이므로 <Output, Failure>를 정의해줘야합니다. 위 코드에서는 String 값을 방출하고 Failure는 일어날 일이 없다! 라고 정의해둔 모습입니다 :)
  2. 생성한 subject의 sink 메서드를 통해 subscriber와 연결을 해줍니다. 이전 게시글에서 publisher와 subscriber를 연결하는 과정과 똑같죠?
  3. subject에 "Hello"와 "World" 두 값을 삽입합니다. (insert라는 단어가 여기서는 좀 애매하게 느껴지네요. 전송 느낌의 삽입?... 으로 저는 이해하고 있습니다😔)
  4. 마지막으로 send(completion:)을 통해 종료되었다는 것을 알려줍니다.

출력값을 확인해보겠습니다.

Hello
World
finished

굉장히 간단하죠? "Hello"와 "World"를 send 했기 때문에 recievedValue에 정의된 closure를 통해서 값이 출력됩니다. 이후에 종료하라는것을 알려줬기 때문에 receiveCompletion에 정의된 closure가 실행되면서 종료가 됩니다.

 

다음은 CurrentValueSubject에 대한 코드를 살펴보겠습니다. PassthroughSubject와 CurrentValueSubject의 차이점은 초기값 설정에 있습니다. PassthroughSubject 예제에서는 초기값 설정이 안돼있었죠? 이번에는 초기값을 설정해봅시다!

// 1
let subject = CurretValueSubject<Int, Never>(1)

// 2
print(subject.value)

// 3
subject.send(2)
print(subject.value)

subject.sink { value in
  print(value)
}
  1. CurrentValueSubject를 생성해 줍니다. 마찬가지로 Int 값을 방출하고 Error는 일어나지 않습니다! 하지만 이번에는 초기값을 설정할 수 있습니다.
  2. value property에 접근하여 현재값을 가져올 수 있습니다.
  3. subject에 2를 삽입합니다. 해당 값을 subject에 현재 값에 저장됩니다.
  4. subject의 sink를 통해 subscriber와 연결합니다.

당연하게도 PassthroughSubject의 경우에는 해당 value property를 제공하지 않습니다. 이름에서 알 수 있듯이 PassthroughSubject는 통과하는 Subject이기 때문에 값이 저장되는 것이 아니라 send된 값을 바로 방출합니다. 하지만 CurrentValueSubject의 경우 현재 값 Subject? 해석이 좀 이상하지만ㅎㅎ.. 현재 값을 저장하고 있습니다.

 

결과를 알아보겠습니다.

1
2
2
finished

위의 결과에서 확인할 수 있는점은 CurrentValueSubject의 경우 subcriber와 연결됨과 동시에 현재 값을 방출하게 됩니다. 헷갈리시면 안되는게 1번과 2번 라인의 출력은 print(subject.value)로 출력한 부분입니다!

 

Publisher-Subscriber 연결의 LifeCycle

Publisher와 Subscriber의 연결을 Subscription이라고도 부릅니다. (앞으로는 Subscription라고 말하겠습니다.) 자! 이제 Subscription의 LifeCycle에 대해 알아봅시다!

let subject = PassthroughSubject<Int, Never>

let token = subject
	.print()
	.sink(receiveValue: { print("received by subscriber: \($0)") })

subject.send(1)

이제 좀 익숙해지셨나요? 간단한 예시인데 print()라는 operator가 추가되었습니다. 공식문서의 정의를 보면 "Prints log messages for all publishing events." 오옹.. 모든 publishing 이벤트에 대한 로그메시지를 출력한다고 합니다. 이 operator를 통해 LifeCycle을 알아봅시다.

 

해당 코드에 대한 출력값을 확인해보면

receive subscription: (PassthroughSubject)
request unlimited
receive value: (1)
received by subscriber: 1
receive cancel

생각보다 출력되는 것들이 많습니다.😒 이제 출력값과 출력 이외의 것들을 추가해서 LifeCycle에 대해 알아봅시다!

 

  • publisher의 subscribe<S>(_:) 메서드를 통해 subscriber와 연결합니다. 우리는 이런 subscribe메서드를 호출한적은 없지만 Combine에서 제공해주는 sink를 사용할 경우 자동적으로 호출되는 것 같습니다 :) subject.subscribe(SomeSink()) 요런식으로 호출이 되겠죠? 뇌피셜이기 때문에 혹시나... 댓글 부탁드립니다😭

 

subscribe<S>(_:) 공식문서에 따르면 

Always call this function instead of receive(subscriber:). Adopters of Publisher must implement receive(subscriber:). The implementation of subscribe(_:) provided by Publisher calls through to  receive(subscriber:).

짧게 말하면 subscribe(_:) 메서드에서 어차피 receive<S>(subscriber: S)를 호출하니까 receive(subscriber:) 대신 subscribe(_:)를 호출해라! 입니다 😂
  • 요청받은 publisher는 receive<S>(subscriber: S)를 호출하여 subscription을 생성합니다. 우리가 호출을 하지 않더라도 위의 설명대로 subscribe(_:)에서 receive(subscriber:)를 알아서 호출하겠죠?
  • publisher는 subscription 요청을 승인합니다. 이제 subscription은 생성이 되었기 때문에 출력문의 receive subscription: (PassthroughSubject)가 출력됩니다. 이후 subscriber의 receive(subscription:)을 호출됩니다.  헷갈릴 수도 있으니 Subscriber 코드를 다시 확인해봅시다! 첫번째 receive가 호출되는거죠!
    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>)
    }​
  • 다음은 request unlimited 로그입니다. 이것은 subscriber는 값들을 얼마나 수신 받을지에 대해 요청했는지에 대해 나타냅니다. 이 요청은 위에서 호출된 receive(subscription:) 메서드에서 안에서 호출됩니다. 예제 코드로 확인해봅시다.
    class SomeSubscriber: Subscriber {
    ... 생략 ...
      func receive(subscription: Subscription) {
        print("구독을 시작합니다.")
        subscription.request(.unlimited)
        // subscription.request(.max(2))
      }
    ... 생략 ...
    }
  • 요런식으로 호출이 됩니다. request(_:)의 인자로 들어가는 것은 Subscribers.Demand입니다. 해당 값을 통해 Publisher가 Subscriber에게 몇 개의 아이템을 방출할지를 결정합니다.

 

Subscribers.Demand의 Apple 공식문서를 확인해보면,

"Subscription을 통해 Publisher가 Subscriber에게 보내는 요청된 아이템의 개수" 라고 합니다.
.max(_:) → 지정된 최대 요소 수에 대한 요청을 생성합니다.
.unlimited → Publisher가 생성할 수 있는 만큼의 요청입니다.
.none → Publisher의 값이 없는 요청입니다.
  • receive value: (1)는 이제 subject.send(1)를 통해 우리가 생성한 subject가 값을 받았다는 의미겠죠?
  • publisher는 subscriber의 receive(_:)를 호출해 값을 전달합니다. recevied by subscirber: 1을 출력합니다. 이 메서드는 subscriber가 얼마나 값을 더 받을 수 있는지에 대한 Demand인스턴스를 반환합니다. subscriber는 이 값을 증가시키거나 값이 같을 때 종료할 수 있지만, 값을 감소시킬 수는 없습니다.
  • 마지막으로 receive cancel을 출력합니다. subscription은 아래의 결과 중 하나로 종료됩니다.
    • 취소되는 경우 → 이 결과는 위의 예제처럼 subscriber가 메모리에서 해제되는 경우 자동으로 일어납니다. 또는 수동적으로 cancel() 메서드를 통해 취소할 수 있습니다.
    • 성공적인 종료
    • 에러를 동반한 실패

 

오늘은 Subject와 Subscription의 LifeCycle에 대해 알아봤습니다. LifeCycle을 알아둔다면 커스텀한 Publisher 또는 Subscriber를 구현해야할 때, 어디에 어떤 코드를 넣어줘야할지 좀 더 명확해지겠죠?

 

다음글은 아마도 AnyCancellable이나 Operator에 관련해서 작성해보려고 합니다. ㅎㅎ 긴 글 읽어주셔서 감사합니다 :)

Comments