Combine 스터디 (1) - Publisher, Subscriber

Seoyoung Lee·2024년 3월 3일
0

Combine 스터디

목록 보기
1/5
post-thumbnail

저번 달부터 회사 동기들이랑 컴바인 스터디를 하고 있다.
나도 정리도 나름 열심히 해가고 도움도 많이 되고 있어서 블로그에도 기록용으로 남겨본다✌🏻

Combine?

  • 비동기 이벤트를 처리하는 애플의 프레임워크.

  • 시간이 지남에 따라 값을 처리하기 위한 선언형 API들을 제공한다.

  • Publisher: 시간에 따라 변할 수 있는 값을 노출

  • Subscriber: Publisher로부터 값을 받고 그에 대한 액션을 취함

  • 여러 publisher들의 값들을 결합하고, 상호작용을 조절할 수 있음

Combine 사용의 장점

  • 이벤트를 처리하는 코드를 중앙화함으로써 가독성, 유지보수
  • 중첩 클로저, convention-based callbacks 등보다 사용하기 편함
    • ‘협약에-기초한 콜백 (convention-based callbacks)’ 은 이벤트를 ‘콜백 (callback) 함수’ 로 처리하려면 양쪽 사이에 서로 ‘협의해서 약속한 (convention)’ 정보가 있어야 함을 의미한다..고 한다.

Publisher

protocol Publisher<Output, Failure>
  • 하나 이상의 Subscriber에게 값을 전달한다.

  • Subscriber의 Input, Failure 타입과 Publisher의 Output, Failure 타입이 일치해야 함

  • receive(_:) 메소드를 호출한 다음에 subscriber의 receive 메소드를 호출할 수 있음.

    • 얘네를 사용해서 subscriber와 커뮤니케이션(?) 할 수 있다.

    • receive 메소드 종류는 3가지!

      → 위의 세 메소드들은 Subscriber 프로토콜 안에 있는 메소드들임.

  • 시간에 따라 값을 방출한다.
  • 에러 상황을 감지하기 위해 에러도 함께 방출한다.
  • 내가 방출할 값과 에러(만약 있으면)의 종류를 정의해야 한다.

Publisher는 프로토콜이기 때문에 원래는 직접 구현해야 한다. 하지만 Combine에서는 다양한 Publisher 타입(?)을 제공한다.

  • Subject의 구체적인 하위클래스 사용하기 (ex: PassthroughSubject) send() 메소드 호출해서 실시간으로 값을 publish 할 수 있다
  • CurrentValueSubject
    • subject의 기본값을 업데이트 할 때마다 값을 publish 할 때 사용
  • 프로퍼티에 @Published 어노테이션 붙이기
    • 프로퍼티의 값이 바뀔 때마다 이벤트를 방출하는 퍼블리셔를 가지게 됨

Publishers..????

Publisher..? Publishers..?? 넌 뭐냐?? 🤯

  • Publisher 역할을 하는 타입을 모아둔 enum
  • Publisher의 extension으로 정의된 Operator들은 Publishers 를 상속(??)하는 클래스나 구조체로 기능을 구현한다.
    • 예: contains(_:) Operator는 Publishers.Contains 인스턴스를 반환한다.

Publisher의 종류

  • Just
    • 가장 기본적인 Publisher
    • 하나의 값만 방출하고, 절대 실패하지 않는다
["Pepperoni", "Mushrooms", "Onions", "Sausage", "Bacon", "Extra cheese", "Black olives", "Green peppers"].publisher

나는 값을 여러 번 방출하고 싶어!! → publisher로 손쉽게 변환할 수 있다.

여러 개의 값을 방출하고 종료함.

.publisher 라는 키워드를 붙여서 publisher를 만들 수 있다

예시 - NotificationCenter 사용

// create the order
let pizzaOrder = Order()
let pizzaOrderPublisher = NotificationCenter
  .default
  .publisher(for: .didUpdateOrderStatus, object: pizzaOrder)
  • pizzaOrderPublisher: NotificationCenter가 pizzaOrder에 didUpdateOrderStatus 이라는 노티피케이션을 보낼 때마다 이벤트를 방출하는 Publisher

// once the user is ready to place the order
NotificationCenter
  .default
  .post(name: .didUpdateOrderStatus,
        object: pizzaOrderPublisher,
        userInfo: ["status": OrderStatus.processing])
  • NotificationCenter에 데이터를 보내고 post → publisher가 값을 방출함
  • 근데 아직 이 publisher를 구독한 subscriber가 없어서 아무 일도 일어나지 않는다.

❓ 궁금한 점..
notification center를 반드시 사용해야 하는가?
어떤 경우에서 사용하는 건가? 라고 생각했었는데
애플 공식문서에 있던 예시는 macOS를 개발하는 AppKit에서 사용하는 방법을 알려주는 예시였다.
나중에 Swift: NotificationCenter와 사용법 를 읽어봐도 좋을 듯!

Subscriber

protocol Subscriber<Input, Failure> : CustomCombineIdentifierConvertible
  • Publisher로부터 요소의 스트림을 받는다.
    • 라이프사이클 이벤트에 따라..
  • Publisher와 타입이 일치해야 한다는 거 유의
  • Publisher의 subscribe(_:) 메소드 호출해서 subscriber와 publisher를 연결할 수 있다.

  • publisher를 구독하고 얘네가 방출한 값을 받음
  • 각 subscriber는 어떤 타입의 값과 에러를 받을 건지 정의해야 한다.

Publisher와 Subsciber 연결 과정

  1. Publisher의 subscribe(_:) 메소드 호출

    func subscribe<S>(_ subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input

    Apple 왈,, receive(subscriber:) 대신 얘를 호출하라고 함. 이 함수 안에서 receive(subscriber:) 를 호출할 거임.

  2. 그러면 publisher가 subscriber의 receive(subscription:) 메소드를 호출하게 됨

  3. subscriber에게 Subscription 인스턴스를 전달해줌

    • Publisher에게 요소를 달라고 요구하는 역할
    • 아니면 구독을 취소할 수도 있음
  4. subscriber가 처음으로 Demand를 생성함

  5. publisher가 subscriber의 receive(_:) 메소드를 호출해서 새로 publish된 요소를 전달함.

  6. publisher가 publish를 중단하면 receive(completion:) 메소드를 호출함

    • Subscriber.Completion 타입의 파라미터 사용
    • 퍼블리싱이 정상적으로 끝났는지 아니면 에러를 뱉었는지 나타내기 위함

❓ receive 메소드를 정리해보자

🤯 아니.. receive 메소드가 publisher꺼야 subcriber꺼야;; 겁나 헷갈리네

💫  Publisher

1️⃣ receive(subscriber:)

func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input

publisher에게 subscriber를 붙이는 메소드

직접 이 메소드를 호출하지 않고 subscribe(_:) 를 호출하면 얘를 호출해준다.

💫 Subscriber

값을 받는 것 / 생애주기 이벤트를 받는 것으로 나뉨

1️⃣ receive(_:)

func receive(_ input: Self.Input) -> Subscribers.Demand

subscriber에게 publisher가 항목을 만들어냈음을 알려주는 메소드

  • input : publish된 요소
  • Subscribers.Demand : subscriber가 얼마나 많은 요소들을 더 받을 것으로 예상하는지를 나타내는 인스턴스

2️⃣ receive()

func receive() -> Subscribers.Demand

void 요소의 publisher가 다음 요청을 받을 준비가 되었음을 알려주는 메소드

이벤트가 발생했다는 것만 알려주고 싶을 때 Void input / output을 사용

3️⃣ receive(subscription:)

func receive(subscription: any Subscription)

subscriber에게 publisher를 성공적으로 구독했고, 항목을 요청할 수 있음을 알려주는 메소드

  • subscription? publisher와 subscriber 사이의 연결을 나타냄. publisher에게 항목을 요청할 때 매개변수로 받은 subscription을 사용함.

4️⃣ receive(completion:)

func receive(completion: Subscribers.Completion<Self.Failure>)

subscriber에게 publisher의 퍼블리싱이 끝났음을 알려주는 메소드

  • Subscriber.Completion : 퍼블리싱이 정상적으로 끝났는지 or 에러를 뱉었는지 알려줌

Combine의 주요 subscriber

Combine은 Publisher 타입에 있는 operator 형태의 subscriber를 제공한다.

  1. sink
    • completion 시그널을 받고 새 항목을 받을 때 클로저를 실행
  2. assign
    • 받은 값을 다른 프로퍼티나 publisher에게 할당

Subscribers

enum Subscribers

Publisher와 마찬가지로 subscriber 역할을 하는 타입들을 모아둔 네임스페이스가 있다.

Demand

  • 요청한 항목의 개수
  • subscription을 통해 subscriber → publisher에게 전달됨

max(_:)

  • publisher에게 몇 번 더 값을 달라고 요청을 보낼 것인지
  • 음수를 전달하면 fatal error가 발생함

unlimited

  • publisher가 만들 수 있는 만큼 다 요청을 보낸다

none

  • publisher로부터 어떤 값도 요청하지 않는다.

Completion

publisher가 정상적인 종료 or 에러 발생에 의해 더이상 값을 만들어내지 않음을 알려주는 신호

finished

  • 정상적으로 종료됨

failure(Failure)

  • 에러에 의해 publisher가 퍼블리싱을 중단함

Convenience Publishers

Just

struct Just<Output>

  • 각각의 subscriber에게 output을 한 번 방출하고 종료되는 Publisher
  • 반드시 값을 하나 생성하고, 에러를 생성하지 않는다. → Failure의 타입이 Never 로 정의되어 있기 때문

기존 Swift에서의 Never?

Never 는 코드에서 실행되면 안되는 부분을 명시적으로 나타냅니다.

Never 를 이용해서 코드를 볼 때 ‘아 이부분은 실행이 되면 안되는 부분이구나’ 라고 판단할 수 있을 것이다. 앱을 강제 종료 해야 한다거나, UIEvent 에 Error 를 써야 할 때 활용할 수 있을 것 같다. 고 한다..

Swift 에서 Never 란 무엇일까?

Never keyword in Swift: return type explained with code examples

Publisher가 에러를 뱉지 않으면 Never 를 사용함

  • publisher의 체인을 시작할 때 사용할 수 있다고 한다.

Future

final class Future<Output, Failure> where Failure : Error

하나의 값을 만들고 결과를 보내는 (종료 or 실패) publisher

  • 하나의 값을 비동기로 publish 해야 할 때 사용 → escaping closure로 구현되어 있다

Future.Promise

typealias Promise = (Result<Output, Failure>) -> Void
  • Future 안에서 호출되는 클로저의 typealias
  • Result<Output, Failure> : Future가 publish한 값 or 에러
public init(_ attemptToFulfill: @escaping (@escaping (Result<Output, Failure>) -> Void) -> Void)

사실상 이렇게 정의되어 있는 것

func generateAsyncRandomNumberFromFuture() -> Future <Int, Never> {
    return Future() { promise in
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            let number = Int.random(in: 1...10)
            promise(Result.success(number))
        }
    }
}

Future는 Swift Concurrency로 대체 가능하다고 하는데,, 나중에 알아보는 걸로~

Deferred

Defer: 미루다, 연기하다

struct Deferred<DeferredPublisher> where DeferredPublisher : Publisher

새로운 Subscriber의 Publisher를 만들기 위해 클로저를 실행하기 전 Subscription을 기다리는 Publisher

  • init의 createPublishersubscribe(_:) 가 호출될 때 실행된다. 이름 그대로 나중에 호출될 Publisher를 새로 만드는 클로저

createPublisher

let createPublisher: () -> DeferredPublisher

createPublisher 에 의해 리턴된 publisher는 subscription을 바로 받는다.

Empty

struct Empty<Output, Failure> where Failure : Error

아무 값도 publish 하지 않고, 즉시 종료될 수 있는 publisher

  • Empty(completeImmediately: false) : 어떤 값도 보내지 않고, 결과(종료 or 실패)도 보내지 않는 “Never” publisher
  • completeImmediately : Publisher가 바로 completion을 보낼지 여부
    • true면 subscriber에게 subscription을 보내고 바로 종료
    • false면 절대 완료되지 않음
  • init(completeImmediately:outputType:failureType:)
    • empty publisher를 subscriber나 특정한 output과 failure 타입이 정해진 publisher와 연결할 때 사용하는 생성자

Fail

struct Fail<Output, Failure> where Failure : Error

특정한 에러와 함께 바로 종료되는 publisher

  • init(outputType:failure:)
    • 특정한 output 타입을 요구하는 다른 subscriber나 publisher와 연결하고 싶을 때 사용하는 생성자

Record

struct Record<Output, Failure> where Failure : Error

여러 개의 input들과 하나의 completion을 기록하고, 나중에 subscriber에게 전달해주는 publisher

  • Record Publisher를 생성할 때 output들을 함께 배열 형태로 매개변수로 전달한다.
  • 기존에 있던 recording 을 가지고도 새로 생성 가능

Recording

Record가 기록한 결과를 보관해두는 역할을 하는 구조체

  • 기록된 output들의 시퀀스
  • Subscriber에게 보낼 output과 completion을 설정함
  • receive(_:) , receive(completion:) 로 각각 새 output과 completion을 저장함
    • output은 completion이 추가된 후에는 생성 불가능
    • completion은 하나만 추가 가능

AnyPublisher

@frozen
struct AnyPublisher<Output, Failure> where Failure : Error

다른 publisher를 감싸서 타입을 지우는 publisher

언제 사용하지?

  • 자세한 타입을 감추고 싶을 때
  • Subjectsend(_:) 메소드가 호출되는 걸 막고 싶을 때 → 값 변경을 막음

eraseToAnyPublisher() 를 사용해서 publisher를 AnyPublisher로 감쌀 수 있다

Convenience Subscribers

Sink

final class Sink<Input, Failure> where Failure : Error

무한정으로 값들을 요청하는 subscriber

  • receiveCompletion: completion에서 실행될 클로저
  • receiveValue: 값을 받았을 때 실행할 클로저

Assign

final class Assign<Root, Input>

받은 항목을 key path를 사용해서 프로퍼티에 할당하는 subscriber

  • Assign의 Failure 타입은 Never 이다.
  • init(object:keyPath:)
    • subscriber는 새 값을 받을 때마다 object 의 프로퍼티에 값을 할당한다.
    • 할당해줄 프로퍼티는 keyPath 를 사용해서 나타낸다. (key-path expression은 여기를 참고하자..)
class SampleObject {
    var intValue: Int {
        didSet {
            print("intValue Changed: \(intValue)")
        }
    }
    
    init(intValue: Int) {
        self.intValue = intValue
    }
    
    deinit {
        print("sample object deinit")
    }
}

let myObject = SampleObject(intValue: 5)

let assign = Subscribers.Assign<SampleObject, Int>(object: myObject, keyPath: \.intValue)

let intArrayPublisher = [6,7,8,9].publisher
intArrayPublisher.subscribe(assign)

AnySubscriber

@frozen
struct AnySubscriber<Input, Failure> where Failure : Error
  • 타입을 지운 subscriber
  • AnyPublisher와 마찬가지로 자세한 타입을 보여주고 싶지 않을 때 사용
  • Subscriber 를 직접 구현하지 않고, 프로토콜 안에 정의된 메소드를 위한 클로저를 사용해서 커스텀 subscriber를 만들고 싶을 때도 사용한다고 한다.. → 많이 사용할 일은 없지 않을까?

Convenience Publishers와 비교?

  • Publisher들은 Publisher 프로토콜을 채택한 구조체 (Future 제외)였음
  • Subscriber들은 Subscribers 안에 정의되어 있고, class 타입이다.

Cancellable

Cancellable

protocol Cancellable

어떤 작업에 대한 취소가 가능함을 나타내는 프로토콜

cancel()

  • 할당된 자원을 모두 해제함
  • 타이머, 네트워크 접근, 디스크 I/O로 발생하는 사이드이펙트를 막을 수 있다

store(in:)

  • cancellable 인스턴스를 저장하는 메소드 → 파라미터로 받은 곳에 저장함

Subscription 프로토콜이 Cancellable 을 채택

→ thread-safe 하기 위해

AnyCancellable

final class AnyCancellable

취소되었을 때 클로저를 실행하는 type-erasing cancellable 객체

  • Subscriber는 “cancellation token”을 갖기 위해 이 타입을 사용할 수 있다 → caller가 publisher를 cancel하기 위해 사용
  • deinit 되었을 때 자동으로 cancel() 을 호출한다.
profile
나의 내일은 파래 🐳

0개의 댓글