 Apple Lover Developer & Artist

영속적인 디자인에 현대의 공감을 채워넣는 공방입니다

 Apple/Apple Dev Documents

[WWDC2019] Introducing to Combine

singularis7 2023. 3. 20. 16:03
반응형

Overview

A unified declarative API for processing values overtime. - WWDC2019 "Introducing Combine 발췌"

앱 개발 과정에서 비동기 프로그래밍을 사용할 때가 많이 있다. 예를 들어 Target-Action, Timer, KVO부터 URLSession을 활용한 네트워크 통신 과정에서 컴플리션 핸들러를 활용하기도 한다. 때로는 이들을 결합해 새로운 기능을 개발해야 할 수 있는데 쉽지 않았다. Combine은 이들간의 공통 분모를 찾게되었으며 "시간이 지남에 따라 값을 처리하기 위한 통합된 선언적 API"를 개발하게 되었다.

특징

Generics

콤바인은 Swift로 작성되었다. 즉, Generics과 같은 Swift 기능을 사용할 수 있다. 이를 통해 상용구 코드를 줄일 수 있게 되었다. 가령, 비동기 동작에 관한 제네릭 알로리즘을 한번 작성해두면 다른 종류의 비동기 인터페이스에도 적용할 수 있게 되었다!!!

Type Safe

콤바인은 타입-세이프하다. 즉, 컴파일 타임에 오류를 잡을 수 있다는 의미이다.

Composition First

콤바인의 주요한 디자인 철학은 Composition을 우선한다는 점이다. 즉, 핵심 컨셉은 단순하고 쉽게 이해할 수 있으며 개념들이 함께 모여서 더 많은 것을 만들 수 있다.

Request Driven

콤바인은 요청 주도 기반이다. 앱의 메모리 사용량과 성능을 관리할 수 있다.

핵심 개념

Publisher

Publisher는 콤바인 API의 선언적 부분이다. 값과 오류가 생성되는 방식을 설명한다. Publisher가 값들을 실제로 생산하는 것은 아니다. Publisher는 Swift에서 구조체를 사용하는 의미로 값타입이다.

Publisher는 Subscriber의 등록도 허용한다. Subscriber는 시간이 지남에 따라 값을 받는다. 다음은 Publisher 프로토콜의 예이다.

Code

protocol Publisher {
  associatedtype Output
  associatedtype Failure: Error

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

Publisher에는 생성되는 값의 종류를 의미하는 연관 타입 Output과 오류의 종류를 의미하는 Failure 두가지 유형이 있다. Publisher가 오류를 생성할 수 없는 경우엔 연관 타입으로 Never를 사용할 수 있다.

Publisher의 핵심 기능으로 subscribe가 있다. 위 코드의 제네릭 subscribe 메소드의 제약조건에서도 볼 수 있지만 Subscriber의 입력과 오류는 Publisher의 출력과 오류와 일치해야 한다.

Example

NotificationCenter 기술에 콤바인 Publisher를 접목한 예시는 다음과 같다.

extension NotificationCenter {
  struct Publisher: Combine.Publisher {
    typealias Output = Notification
    typealias Failure = Never
    init(center: NotificationCenter, name: NotificationName, object: Any? = nil)
  }
}

Publisher는 구조체로 선언되었으며 Output 타입은 Notification이고 Failure 타입은 Never이다. Publisher가 생성될 때는 center, name, object 정보가 필요하다. 기존의 NotificationCenter API와 매우 유사하다. 핵심은 기존의 Notification 기술을 대체하지 않고 Combine에 적응(adapting) 시키고 있다는 점이다.

Subscriber

Subscriber는 Publisher의 상대편에 위치한다. Subscriber는 Publisher가 유한한 경우 completion을 포함하여 값을 받는다. Subscriber는 보통 값을 수신할 때 자신의 상태를 변경하며 작동하기에 Swift의 참조 타입을 사용한다. 즉, 클래스를 활용한다는 것을 의미한다.

Code

protocol Subscriber {
  associatedtype Input
  associatedtype Failure: Error

  func receive(subscription: Subscription)
  func receive(_ input: Input) -> Subscribers.Demand
  func receive(completion: Subscribers.Completion<Failrue>)
}

Subscriber에도 두가지 연관 타입이 있다. Input과 Failure가 보인다. 여기서도 Subscriber가 Failure를 수신할 수 없는 경우 Never 타입을 사용하게 된다.

3가지 receive 메서드가 보인다. Subscription을 받을 수 있는데 Subscription은 Subscriber가 Publisher에서 Subcriber로의 데이터 흐름을 제어하는 방법이다. 물론 input도 받을 수 있으며 Publisher가 유한한 경우 completion 혹은 failure를 받을 수도 있다.

Example

다음은 Subscriber 중 Assign 예시 코드이다.

extension Subscribers {
  class Assign<Root, Input>: Subscriber, Cancellable {
    typealias Failure = Never
    init(object: Root, keyPath: ReferenceWritableKeyPath<Root, Input>)
  }
}

Assign은 클래스이다. 클래스의 인스턴스, 객체의 인스턴스 및 객체에 대한 type-safe key path로 생성된다. Assign 구독자가 하는 일은 입력을 받으면 해당 객체의 해당 속성에 입력을 쓰는 것이다. Swift에서 프로퍼티의 값을 작성하는 경우 오류를 처리할 방법이 없기 때문에 Assign의 Failure 타입은 Never로 설정한다.


The Pattern

  1. 컨트롤러 계열의 객체 혹은 Subscriber를 들고 있는 객체는 Publisher와 Subscriber를 연결해주는 subscribe를 호출하여 붙일(attach) 책임이 있다.
  2. Publisher는 Subscriber로 부터 유한하거나 무한한 요청을 만드는데 사용할 subscription을 Subscriber에게 보낸다. Subscription은 Subscriber가 Publisher에게 특정 수 혹은 무한한 값을 요청하는데 사용될 것이다.
  3. Publisher는 Subscriber에 구독 개수 이하의 값을 구독자에게 자유로이 보낼 수 있다.

img


Operator

NotificationCenter에 Combine을 접목해보자! Publisher와 Subscriber만 사용할 경우 Swift의 제네릭 개념을 통한 컴파일 타임의 오류를 만날 수 있다. 예를 들어 어떤 값이 변경되는지 감시하다가 변경되면 Notification을 보내서 특정 객체의 프로퍼티를 변경하는 구현을 생각해볼 수 있을 것이다.

Publisher와 Subscriber 사이의 Subscription 관계를 만들어주려면 프로토콜 정의에 걸려있는 제네릭 제약 조건에 의해 서로 소통할 때 사용되는 값의 타입이 일치해야 한다. 하지만 일치하지 않는 경우도 있을 수 있단 점이다.

Publisher와 Subscriber 사이에서 값을 변환하여 호환될 수 있도록 도와주는 것이 Operator이다.

특징

  • Operator 는 Publisher 프로토콜을 채택할 때까지 Publisher이다.
  • Operator는 선언적이며 값 타입이다.
  • Operator가 하는 일은 값의 변경, 추가, 제거 혹은 여러 종류의 행위를 의미힌 동작을 설명하는 것이다.
  • upstream Publisher를 구독하고 그 결과를 downstream Subscriber에게 전달한다.

Example

다음은 연산자의 예이다. Map을 소개한다. 콤바인에서도 아주 친숙하게 사용할 수 있을 것이다.

extension Publishers {
  struct Map<Upstream: Publisher, Output>: Publisher {
    typealias Failure = Upstream.Failure

    let upstream: Upstream
    let transform: (Upstream.Output) -> Output
  }
}

Map은 연결하는 upstream과 upstream의 출력을 자체 출력으로 변환하는 방법으로 생성되는 구조체이다. Map은 자체적으로 Failure를 생성하지 않기 때문에 단순히 upstream의 Failure 타입을 전달하기만 한다.

앞서 소개한 Publisher와 Subscriber 사이 입출력 타입 호환 문제를 Map을 활용해 해결할 수 있다. 위 정의만 보면 Map이라는 별도의 Publisher 구조체 인스턴스를 생성해줘야 할 것처럼 생겼다. 좀더 편하게 쓸 수 있는 방법이 존재한다. 다음을 살펴보자!

extension Publisher {
  func map<T>(_ transform: @escaping (Output) -> T) -> Publishers.Map<Self, T> {
    return Publishers.Map(upstream: self, transform: transform)
  }
}

이미 Publisher 프로토콜의 extension 을 통해 기본 구현으로 map 연산자가 존재한다. 즉, 모든 Publisher에서 map을 손쉽게 호출할 수 있다는 의미이다. 이는 비동기 프로그래밍의 생각을 바꿔줄 것이다. 예를 들어 다음을 살펴보자!

let cancellable = 
    NotificationCenter.default.publisher(for: .graduated, object: merlin)
        .map { note in
      return note.userInfo?["NewGrade"] as? Int ?? 0
    }
        .assign(to: \.grade, on: merlin)

알림을 받으면 클로져를 활용해 맵핑한 후 Merlin 객체의 grade 프로퍼티에 할당해주는 코드이다. 놀라운 점은 단계별로 발생하는 일에 대해 매우 선형적이고 이해하기 쉬운 흐름을 제공해준다는 점이다.

Assign Subscriber는 Cancellable을 반환해준다. 콤바인에는 취소 기능도 내장되어있다. 취소 기능을 사용하면 Publisher와 Subscriber에서 내려오는 sequence를 조기에 해제할 수 있다.

Declarative Operator API

Map과 같은 Operator를 많이 갖고 있다. Apple은 이들을 "Declarative Operator API" 라고 부른다.

  • Functional transformation -> Map, Filter, Reduce
  • List Operation -> first, second, fifth element of Publisher
  • Error Handling -> 오류를 기본값 혹은 배치값으로 변경
  • Thread or queue movement -> 무거운 처리작업을 백그라운드로 UI작업을 main으로 이동
  • Scheduling and time -> 루프와의 통합, 디스패치 큐, 타이머 지원, 타임아웃 등

Try composition first

연산자가 많아서 찾는 것이 어려울 수 있다. 이 경우 콤바인의 핵심 디자인 원칙으로 돌아가는 것을 권장한다. 바로 Composition이다.

많은 작업을 수행하는 몇가지 연산자를 제공하기보다는 각각 조금씩만 작업을 수행하는 연산자를 제공하기에 쉽게 이해할 수 있다.

Swift의 Collection API에서 영감을 얻어 사용자가 연산자를 쉽게 찾을 수 있도록 지원한다. 다음의 표를 살펴보자.

  Synchronous Asynchronous
One Int Future
Many Array Publisher

쉽게 말해 배열로 수행하는 방법을 이미 알고 있는 특정 작업을 찾고 있다면 Publisher에서 시도해보라는 의미이다. 예를 들어 nil값을 거르기 위해 compactMap 등의 연산자를 활용해볼 수 있다.

Cobining Publisher

map 등의 연산자가 동기식 환경에서 유용했다면 비동기 환경에서 빛을 발하는 콤바인 연산자도 있다. 예를 들어 Publisher를 결합하는 경우가 있다.

Zip 연산자를 통해 특정 입력들을 하나의 튜플값으로 묶을 수 있다. 예를 들어, A와 B와 C가 완료되면 다른 작업을 수행하세요 등을 표현할 수 있는 것이다.

Reference

반응형