이재용의 iOS

Hot vs Cold Publisher

2024년 4월 12일 • ☕️ 2 min read

Combine을 사용해보았더라면 각 성격과 구현에 따라 Publisher들이 서로 많이 다르다는 것을 알 수 있다. 선언적 UI에서 비동기 코드를 래핑하여 사용 가능한 Future, publisher들의 수명 주기와 이벤트 흐름을 제어할 수 있는 Subject 그리고 연산들을 도와주는 Operator가 있다. 이들을 잘 살펴보면 API 호출 등과 같이 비동기 작업의 응답을 수신하면 완료되는 Publisher가 있는 반면 수신하지 않고도 무기한 값을 계속 수신할 수 있는 Publisher도 있다. 여기서 핵심은 “완성”이다. 이것으로 Hot Publisher와 Cold Publisher로 나뉜다.

2 types of Callback

콜백에는 크게 두가지가 있다.

  • 특정 트리거가 없더라고 기다리는 콜백 (Cold)
  • 특정 트리거가 있고 1회 돌아와서 완료되는 콜백 (Hot)

예를 들면, 핸드폰에 전화가 오기를 하염없이 기다리는 행위는 첫번째 콜백이다. 하지만, 누군가에게 전화를 걸어 전화를 받기를 기라리는 행위는 두번째 콜백이다. 차이점은 간단하다. 첫 번째는 작업을 실행할 때 이를 트리거할 필요가 없이 어떤 이벤트가 발생하면 자연스레 알게 되지만, 두 번째에서는 실행할 작업을 직접 요청하고 단 한 번만 응답을 기다린다.

Hot and Cold in Publisher

Combine은 escaping closure와 delegate pattern 대신 Subjecter/Publisher를 이용하여 비동기 통신을 도와준다. Publisher는 3가지 종류의 정보를 방출할 수 있다.

  • Values: Publisher로부터 받을 수 있는 결과
  • Error: 비동기 작업에서 발생할 수 있는 오류
  • Completion: 구독이 성공적으로 종료되었다는 이벤트

Cold Publisher

대표적인 Cold Publisher인 Future을 예시로 들어보자. Future Publisher는 명령형 코드를 실행하고 콜백으로 success 또는 error와 함께 promise형태로 응답이 들어온다. 여기서 명령형 코드는 구독이 이루어질 때만 실행된다는 것이 특징이며 응답이 도착하면 구독이 완료된다. (한 번만 실행된다.)

let futurePublisher = Future<Data, Error> { promise in
  Task {
    do {
      let data = try await fetchUserDataAsync()
      promise(.success(data))
    } catch {
      promise(.failure(error))
    }
  }
}

futurePublisher
  .sink { completion in
    switch completion {
    case .finished:
      print("finished!")
    case .failure(let error):
      print("error: \(error)")
    }
  } receiveValue: { value in
    print(value)
  }
  .store(in: &cancellables)

Hot Publisher

위와 달리 Hot Publisher는 구독을 사용하여 작업하고 이벤트를 내보내지 않는 비동기식 흐름을 구성한다. 다른 Publisher와 마찬가지로 완료로 완료될 수 있지만 여러번 방출될 수 있다.

대표적으로는 Subject이 있다. Subject는 구독자가 어떻게 구현되어있는 지 어디로 이벤트를 방출시키고 있는 지는 모른다는 것이 특징이다. 코드에서 볼 수 있듯이 completed를 받지 않는 이상 여러번 방출될 수 있다.

let passthroughSubject = PassthroughSubject<String, Never>()

passthroughSubject.sink(
    receiveCompletion: { completion in
        switch completion {
        case .finished:
            print("Received finished")
        case .failure(let error):
            print("Received error: \(error)")
        }
    },
    receiveValue: { value in
        print("Received value: \(value)")
    }
)
.store(in: &cancellables)

passthroughSubject.send("First value")
passthroughSubject.send("Second value")
passthroughSubject.send(completion: .finished) // 명시적으로 .finished 방출

 
// 결과
Received value: First value
Received value: Second value
Received finished (데이터 스트림 종료)