안녕하세요.
Publihser와 Subscriber을 공부하면서 배운 개념들을 소개합니다.
What is Pubilsher?
이전 파트에서 소개 했지만 다시 간력하게 설명하겠습니다. "시간의 흐름"에 따라 Publisher's Output type의 값을 하나 또는 여러 Subscriber에게 publish(전송) 전송할 수 있습니다. 주의해야 할 것은 Publisher's Output, Failure generic type 과 Subscriber's Input, Failure generic type이 일치해야만 Publisher는 Subscriber에게 값을 전달할 수 있습니다.
지금 자판기가 있습니다. 자판기에는 많은 음료수가 있습니다. 많은 사람이 자판기를 이용하기 위해 줄을 서 있습니다. 음료수를 마시고 싶어 하는 사람이 자판기에 원하는 만큼 버튼을 누를 경우 해당 음료가 나옵니다. 사람마다 음료를 뽑고 싶어하는 개수가 다릅니다. 자판기에 음료가 떨어진다면 더 이상 음료를 뽑을 수 없습니다. 주인이 다시 자판기에 음료를 넣지 않는 이상 불가능 합니다.
Publisher는 자판기와 같습니다. 값(음료)을 가지고 있어야 특정 시간대에 자판기를 이용하려는 사람(Subscriber)에게 사람이 요구하는 만큼의 음료(값)를 줄 수 있습니다.
Swift에서 Publisher는 프로토콜입니다. 따라서 이를 채택하는 Convenience Publishers로 아래와 같은 publisher가 있습니다. Swift에선 어떻게 자판기(Publisher)를 만들 수 있을까요?
(참고로 이들은 Convenience Publisher이기 때문에 Publishers.sequence 등 Publihsers를 붙이지 않아도 됩니다!)
이 중 몇개의 Publisher를 사용해서 publisher를 만들려고 합니다. 특정 버튼에 위치한 음료의 음료수 바코드를 1,2,3,4,5,6 이라고 가정하겠습니다. ( 뽑아 먹을 수 있는 음료수 6개가 있다는 것입니다!! )
let requestBeverage = Notification.Name("RequestBeverage")
let publisher1 = [1,2,3,4,5,6].publisher
let publisher2 = Just([1,2,3,4,5,6])
let publisher3 = Future<[Int],Never> { promise in
promise(.success([1,2,3,4,5,6]))
}
let publisher4 = NotificationCenter.default.publisher(for: requestBeverage, object: nil)
NotificationCenter.default.post(name: requestBeverage, object: [1,2,3,4,5,6])
class temp {
@Published var publisher5 = [1,2,3,4,5,6]
}
이런 형식의 publisher를 선언할 수 있습니다. 모두 6개의 음료. 각각 1,2,3,4,5,6 바코드가 새겨진 음료가 있습니다. Subject를 통해서도 pubhser를 만들 수 있지만 생략했습니다. 공통점으로 누군가가 와야 자판기에서 음료가 publish될 수 있습니다. 이때 음료를 받을 누군가는 Subscriber입니다.
What is Subscriber?
sequence의 값을 가지고 있는 publisher에게 subscriber를 구독할 경우 publisher로부터 값을 받을 수 있습니다.
기본적으로 Subscriber를 커스텀해서 만든다고 한다면 Publisher's <Output, Failure> Generic type 과 일치하는 Input,Failure타입을 선언해야 합니다.
Publisher와 Subscriber가 값을 주고 받기 위해서는 subscription이 있어야 합니다. Subscriber는 Publisher에게 subscription을 통해 원하는 값의 개수를 Demand할 수 있고 Publisher는 Subscriber에게 subscription을 통해 값을 전달합니다. 여기서 등장하는 subscription은 Subejct object에서 매우 중요한 요소입니다!!
final class ThirstyPeople: Subscriber {
typealias Input = Int
typealias Failure = Never
//1
func receive(subscription: Subscription) {
print("subscription을 통해 연결 OK")
subscription.request(.max(3))
}
//2
func receive(_ input: Int) -> Subscribers.Demand {
print("subscription을 통해 음료수 한개 받음. :\(input)")
return .none
}
//3
func receive(completion: Subscribers.Completion<Never>) {
print("subscription 해제")
}
}
var people = ThirstyPeople()
publisher1.subscribe(people)
publisher1는 publish할 수있는 값을 6개 가지고 있습니다. ThirstyPeople통해 Subscriber를 구현했습니다.
초기에 Publisher와 Subscriber가 연결될 때, receive(subscription:)의 인자값 subscription의 subscription.request()를 통해 publisher가 pubilsh 할 수 있는 값을 몇 개 받을지 지정 할 수 있습니다. 이때 subscription과 최초 연결 되었다는 코드를 작성함으로 Publisher와 Subscriber가 연결 되었음을 알 수 있는 코드도 작성할 수 있습니다.
어떤 값을 받은 시점에서 publisher에게 값을 더 많이 달라고 요구할 수도 있습니다. recieve(_:) 구현할 때의 반환값을 .none이 아닌 .max() or .unlimited로 하면 됩니다.
아직 subscription이 3개나 남았지만 subscriber가 더 이상 값을 받고 싶지 않을 때는 people.reveice(completion: .finished)를 호출하면 됩니다. 또는 publisher가 publish할 수 있는 값 6개를 전부 받으면 됩니다.
Subscriber를 부착할 때 매번 프로토콜을 채택해서 구현하는게 아니라 더 쉬운 방법도 존재합니다.
- sink(_:_:)
- assign(to:on:)
- assign(to:)
이들 중 제가 가장 많이 사용하는 sink를 알아보겠습니다!!
"엥? subscriber가 왜 안보이지? " 처음 공부했을 때 엄청 당황했습니다. publisher가 publish하는 값을 받으려면 subscriber와 같은 존재가 있어야 하는데 WWDC의 Combine 소개 영상에선 Publisher, subscriber등을 소개한 후 예시에서 달랑 이 함수를 통해 publisher를 구독했기 때문입니다.
sink(_:) 함수는 클로저를 통해서 subscriber를 부착하는 형태입니다. 간단합니다!
publisher1로부터 .finish()를 받게 된다면 아무리 publisher1에 값을 추가해도 sink 함수를 통해 값을 받을 수 없습니다. (subscription이 연결 해제 됬기 때문입니다.) publisher1가 현재 갖고 있는 모든 값을 publish해도 .finish()를 통한 completion되지 않고 publisher1가 publish할 수있는 새로운 값을 얻게 되면 부착된 subscriber에게 publish할 수 있는 방법은 없을까요? 이와 관련된 게 Subject입니다!!
sink(_:)를 사용하면 subscriber의 demand를 커스텀 하지 않았기에 publisher1로부터 emitted 값을 전부 받게 됩니다. 몇 개만 받을 수 없을까요? 이때 사용하는 것이 operator입니다. publisher는 oerator chains를 통해 값을 다양하게 조절할 수있습니다. subscriber에서 값을 3개만 받도록 sink를 사용하기 위해선 prefix를 사용하면 됩니다.
publisher1
.prefix(3)
.sink { _ in
print("subsciription 해제")
} receiveValue: { input in
print("subscription을 통해 음료수 한개 받음 : \(input)")
}
만일 publisehr 타입이 Subject인 경우
이와 같이 커스텀 subscriber를 등록한 후에 sink(_:) 함수를 사용할 수도 있습니다.
publisher와 subscriber의 개념에 대해 간략하게 설명했습니다.
의문점이나 틀린 곳이 있을 경우 알려주시면 감사합니다!!