본문 바로가기
iOS/Combine

Introducing Combine

by 바등쪼 2023. 10. 23.

https://developer.apple.com/videos/play/wwdc2019/722/

 

Introducing Combine - WWDC19 - Videos - Apple Developer

Combine is a unified declarative framework for processing values over time. Learn how it can simplify asynchronous code like networking,...

developer.apple.com

 

 

예제 앱

  • 사용자 이름과 비밀번호를 입력 받아 회원가입을 해야하는 간단한 요구사항이 있다.

 

이름을 입력하게 되면 위의 사진처럼 많은 비동기 동작이 진행된다.

  • Target/Action을 사용해 사용자가 입력하는 것에 대한 notification을 수신한다.
  • Timer를 사용하여 사용자가 타이핑을 잠시 멈출 때까지 기다려서 과도한 네트워크 요청을 방지한다.
  • KVO를 사용하여 비동기 작업에 대한 진행률 업데이트를 수신한다.

입력을 계속하다 보면 UI가 이에 맞추어 업데이트된다.

실제로는 더 많은 비동기 작업을 수행한 것이다.

  • URL Session Reqeust에 대한 응답을 기다렸다.
  • 그 결과를 동기식 검사 결과와 Merge 해야 했다.
  • 이 모든 작업이 완료되면 KVC(Key Value Coding)와 같은 것을 사용하여 UI를 다시 업데이트했다.

 

Asynchronous Interfaces

실제로 Cocoa SDK 전체에는 많은 비동기 인터페이스가 존재한다.

  • Target/Action
  • Notification Center
  • URLSession
  • Key-value observing
  • Ad-hoc callbacks
  • delegate
  • 기타

Combine을 통해 다양한 비동기 처리 인터페이스들의 공통점을 찾기 시작했다.

 

 

Combine

시간 경과에 따른 값 처리를 위한 통합 선언적 API


Combine은 Swift를 위해 Swift로 작성되었다.

  • 따라서 Generic과 같은 Swift features를 사용할 수 있다.
    • 제네릭을 사용하면 작성해야 하는 boilerplate를 줄일 수 있다.
    • 또한 비동기 동작에 대한 제네릭 알고리즘을 한 번만 작성하면 모든 종류의 다양한 비동기 인터페이스에 적용할 수 있다.
  • Combine은 type safe하다.
    • 런타임이 아닌 컴파일 타임에 에러를 잡아낼 수 있다.
  • Combine의 주요 설계 포인트는 composition first이다.
    • 즉, 핵심 개념은 간단하고 이해하기 쉽지만 이를 조합하면 부분의 합보다 더 큰 무언가를 만들 수 있다.
  • Combine은 request-driven 기반이므로 앱의 메모리 사용량과 성능을 보다 신중하게 관리한다.

 

 

Key Concepts

1. Publishers
2. Subscribers
3. Operators

 

 

1. Publishers

  • 퍼블리셔는 Combine API에서 선언적인 부분이다.
  • value와 error가 생성되는 방식을 기술한다. (하지만 실제로 값을 생성하는 것은 아니다.)
  • Value Type이다. (struct 사용)
  • Subscriber의 등록을 허용한다.

  • Publisher는 프로토콜이며 위와 같다.
  • 두 가지 associatedType이 존재한다.
    • Output은 퍼블리셔가 생성하는(produce) 값이다.
    • Failure는 퍼블리셔가 생성하는 에러의 종류이다.
      • 퍼블리셔가 에러를 생성할 수 없는 경우에는 associatedtype Failure를 Never로 설정할 수 있다.
  • 퍼블리셔의 주요 기능은 subscribe 이다.
    • Subscribe는 Subscriber의 Input이 퍼블리셔의 Output과 일치해야 하고 Subscriber의 Failure도 퍼블리셔의 Failure와 일치해야 한다.

 

  • NotificaitonCenter의 Publisher 예시이다.
  • Output은 Notification이 Failure는 Never이다.
  • 기존에 NotficationCenter API에 익숙하다면 매우 친숙할 것이다.
  • 즉, Combine은 NotificationCenter를 대체하는 것이 아니라 이것을 adapting 할 뿐이다.

 

2. Subscriber

  • Subscriber는 퍼블리셔에 대응하는 개념이다.
  • 퍼블리셔가 유한한 경우 completion 을 포함하여 value를 수신하는 역할을 한다.
  • Subscriber는 일반적을 값을 수신하면 동작하고 상태를 변경하기 때문에 Swift에서는 Reference 타입을 사용하여 이는 class라는 의미이다.

  • Subscriber의 프로토콜은 위와 같다.
  • 퍼블리셔처럼 2개의 associatedType이 존재한다.
    • Intput은 수신할 값의 타입이다.
    • Failure는 수신할 에러 타입이다.
      • Subscriber가 에러를 수신할 수 없는 경우 Never를 사용하면 된다.
  • 3가지 주요 기능이 있다.
    • receive(subscription: Subscription)
      • 구독(Subscription)을 받을 수 있다.
      • Subscription은 subscriber가 퍼블리셔에서 구독자(subscriber)로의 데이터 흐름을 제어하는 방법이다.
    • receive(_ input: Intput) -> Subscribers.Demand
      • 값을 수신한다. (Input 타입)
    • receive(completion: Subscribers.Completion<Failure>)
      • 퍼블리셔가 유한한 경우 .finished 또는 .failure를 수신할 수 있다.

 

Subscriber의 예시인 Assign이다.

  • Assign은 class이다.
  • 객체의 인스턴스 및 해당 객체에 대한 type safe key path로 초기화(init)된다.
  • 이 클래스가 하는 일은 input을 수신하면 object의 keyPath에 해당되는 프로퍼티에 write하는 것이다.
  • Swift에서는 프로퍼티에 write를 할 때 에러를 처리할 방법이 없기 때문에 할당 실패 타입을 Never로 설정했다.

 

 

 

Publisher와 Subscriber의 관계

Subscriber를 보유하는 일종의 Controller 객체 또는 기타 타입이 있을 수 있으며 이 객체는 Subscriber를 첨부하여 퍼블리셔에게 subscribe(_ subscriber:)을 호출하는 역할을 담당한다.

 

이 시점에서 퍼블리셔는 구독자(subscriber)에게 구독을 전송하고 구독자는 이를 사용하여 퍼블리셔에게 특정 수 또는 무제한의 값을 요청할 수 있다. (Demand)

 

이 시점에서 퍼블리셔는 구독자에게 요청 받은 개수 이하의 값을 자유롭게 보낼 수 있다.

퍼블리셔가 유한한 경우 결국 completion이나 error를 보내게 된다.

 

 

예제

  • Wizard 클래스를 생성한다.
  • NotifcationCenter.Publisher를 생성한다.
  • Subscriber를 생성한다.
  • subscribe(_:)를 호출하여 퍼블리셔와 구독자를 attach하려고 했지만 컴파일 에러가 발생한다.

  • 타입이 매칭되지 않아서 에러가 발생한 것이다.
  • NotificationCenter는 Notifcation을 만든다.
  • 그러나 Assign은 Input 타입으로 Int를 받아야 한다. (위의 코드에서 grade가 Int이기 때문)
  • 따라서 중간에 notifications를 Interger로 변환하는 것이 필요하다.
  • 바로 이것이 Operator이다.

 

3. Operator

  • Operator(연산자)는 Publisher 프로토콜을 채택하기 전까지는 퍼블리셔이다.
  • 선언적이기 때문에 Value type이다. (Value semantic)
  • Operator가 한느 일은 값 변경, 추가, 제거 등 다양한 동작을 한다.
  • Upstream이라고 하는 다른 퍼블리셔를 구독하고 그 결과를 Downstream이라고 하는 구독자에게 보낸다.

 

Operator의 대표적인 예인 Map이다.

  • Map은 연결할 업스틀미과 업스트림의 출력을 자신의 출력으로 변환하는 방법으로 초기화(init)되는 구조체이다.
  • 자체적으로 Failure를 생성하지 않기 때문에 Upstream의 Failure 타입을 미러링하여 전달한다.

Map을 사용하면 notifications를 interger로 변환할 수 있다.

 

 

  • Map 객체를 생성하여 converter를 만들었다.
  • transform 클로저에 notification 타입을 Int로 바꾸는 과정을 구현한다.
  • 이제 subscribe(_:)를 호출하면 컴파일이 잘 된다.

물론 동작은 잘 하지만 위의 구문이 다소 장황하기 때문에 더 fluent한 방식도 존재한다.

이렇게 Publisher 프로토클을 확장하여 각 Operator 이름을 딴 일련의 함수를 추가한 것이다.

map 함수의 아규먼트는 Map 객체를 초기화하기 위한 모든 것을 포함하고 있으며 Upstream만 없는데 그 이유는 Publisher의 확장이기 때문에 간단하게 self를 사용할 수 있기 때문이다.

 

이러한 방식은 단순히 사소한 편의성처럼 보일 수 있지만 실제로는 앱에서 비동기 프로그래밍에 대한 사고 방식을 바꿀 수 있는 기능이다.

(메서드 체이닝 형식으로 선언적으로 구현 가능해진다!!)

 

 

최종 코드

앞서 배운 것들을 활용하면 위처럼 축약해서 코드를 작성할 수 있다.

  1. NotificationCenter 퍼블리셔를 생성
  2. 1의 퍼블리셔에 map을 사용하여 value를 Int로 변환
  3. 2의 다운스트림에서 수신할 Int 값을 곧박로 merlin의 grade 속성에 할당

assign 함수는 Cancelable을 리턴한다.

Cancelation은 Combine에도 내장되어 있다.

cancel을 하면 필요한 경우 퍼블리셔와 구독자의 sequence를 조기에 분리할 수 있다.

 

위의 구문은 단계별로 어떤 일일 일어나는지 매우 선언적이고 이해하기 쉬운 흐름을 제공한다.

각 단계는 체인에서 다음 명령어 집합을 describe한다.

첫 번째 퍼블리셔로부터 일련의 연산자들을 거쳐 Subscriber로 끝나는 일련의 과정을 거치면서 값을 변환한다.

그리고 이러한 Operator 들이 많이 준비되어 있다.

이를 Declarative Operator API이다.

 

 

Declarative Operator API

이렇게 많은 Operator들이 있기 때문에 그 사이에서 어떤 것을 사용하고 탐색할지 고민하는 것이 다소 부담스러울 수 있따.

그래서 우리에게 필요한 것은 Combine의 핵심 설계 원칙인 "Compostion"으로 돌아가는 것이다.

 

많은 작업을 수행하는 몇 가지 연산자를 제공하는 것이 아니라, 각각 조금씩만 수행하는 많은 연산자들을 제공하여 이해하기 쉽게 만들었다.

 

동기식 API와 비동기식 API를 사분면 그래프로 나타내면 위와 같다.

비동기 프레임워크인 Combine에서는 단일 값은 Future로 다수의 값은 Publisher로 표현한다.

 

 

앞서 나온 예제 코드를 여러 Operator들을 사용하여 재구성했다.

  • CompactMap을 사용하여 매핑과 동시에 nil을 제거한다.
  • filter를 사용하여 조건에 맞는 value만 다운스트림에 내보낸다.
  • prefix를 사용하여 정해진 개수만큼만 다운스트림에 내보낸다.
  • assign을 사용하여 merlin의 grade 속성에 할당한다.

 

 

Combining Publishers

  • map과 filter는 유용하지만 주로 동기식 동작을 위한 것이다.
  • Combine은 비동기 환경에서 작업할 때 훨씬 더 유용하다.
  • 이것에 해당되는 대표적인 연산자가 zip과 combineLatest 이다.

 

zip

이 사진처럼 3개의 작업이 모두 완료되면 Continue 버튼이 활성화되게 하고 싶다면 zip 연산자가 유용하게 활용될 수 있다.

Zip은 여러개의 upstream을 하나의 tuple로 변환한다.

  • 모든 Upstream의 입력이 필요하다.
  • 첫 번째 퍼블리셔가 "A"를 생성하고 두 번째 퍼블리셔가 1을 생성하면 zip은 tuple을 생성하고 ("A", 1)을 다운 스트림으로 보낸다.

 

이 예시에서는 이렇게 Zip3를 사용하여 3개의 업스트림으로부터 값을 받아와 버튼을 활성화 할 수 있다.

 

 

Combine Latest

이 사진처럼 일련의 약관들을 전부 동의해야 Play 버튼이 열리도록 하고 싶다면 CombineLatest가 적합하다.

전부 동의를 하고 나서 다시 하나를 비동의로 바꾸면 당연히 Play 버튼은 비활성화 돼야 한다.

 

Zip과 마찬가지로 여러 개의 업스트림을 하나의 값으로 변환한다.

하지만 차이점이 있다!

각 업스트림에서 받은 마지막 값을 저장하고 있다가 업스트림들 중 하나라도 새 값을 방출하면 다른 퍼블리셔들이 가장 마지막에 방출한 값들과 합쳐 tuple로 만들어 다운 스트림에 전달한다.

모든 스위치가 true 일 때만 버튼을 활성화 시키도록 한 코드이다.

 

 

Combine을 적용하자~!

Combine은 앱에 점진적으로 적용할 수 있도록 설계되었다.

notificationCenter나 네트워크 작업, Decode 등에서 Combine을 적용해 보자!

댓글