본문 바로가기

iOS/Combine Framework

[Swift] No3. Hi subject! PassthroughSubject vs CurrentValueSubject 개념 완벽 뿌수기 | Combine

728x90

 Swift Combine. Subject PassthroughSubject, CurrentValueSubject에 대한 개념을 정리하려고 합니다 : ]

  What is Publiser?

Subject를 소개하기 전에 잠깐,, 이전에 소개했던 publisher 예제입니다.

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의 공통적인 특징은 subscriber에게 값을 전달하기 위한 sequence of values는 publisher가 포함하고 있어야 합니다. Sequence를 publish하기 위해선 subscriber가 반드시 구독하고 있어야 합니다. 중요한 것은 한 번 publisher가 sequence value들을 전부 emit 한 후에 completion event를 호출할 경우 더 이상 다른 값을 subscriber에게 전송하지 않습니다. (publisher 제외)

 

let publiser1 = [1,2,3,4,5,6].publisher
publiser1
    .sink { _ in
        print("DEBUG: Publisher1 finished")
    } receiveValue: {
        print("DEBUG: Received \($0) value from subscription.")
    }

 

 간단한 예입니다. publisher1과 publisher2 같은 경우 자신이 갖고 있는 값을 전부 subscriber에게 전송 했다면 completion event를 전송합니다.  물론 글의 후반부에 CurrentValueSubject를 통해서도 [1,2,3,4,5,6]에 대해 선언하고 다루며 차이점을 설명할 것입니다.

 

결과1

publiser로부터 전달받은 subscription의 값을 모두 전송할 경우에 completion event를 전송합니다. 그 이후에 더 이상 값을 전달하지 않습니다.

 왜 그럴까요?

우선 배열은 Sequence 내부 변수로 publisher가 존재합니다.

 

 

 Publishers.sequence타입 입니다. 배열에 ". publisher"를 chain으로 연결할 경우 non-Combine framework의 요소인 sequence의 elements는 Publishers.Sequence의 "Elements"가 generic type로 초기화됩니다.(배열 -> Elements) Elements.Element는 Output type입니다.

 

 이후 Publishers.Sequence에선 Elements의 element를 subscriber에게 emit합니다. 전부 emit 할 경우 곧바로 ". finish"를 호출합니다. 따라서 publisher1가 Elements의 값을 다 전송했다면 sink(_:receive:) 코드 이후의 라인에 아무리 배열에 값을 추가해도 publisher로써의 역할을 하지 않는 것입니다. 물론 append(_:)를 사용해서 배열에 값을 추가해도 publihser1의 Elements에는 반영이 안 됩니다.

publisher1에 값을 추가해도 subscriber에게 추가 emit x

publisher1.append(7)의 경우 새로운 Publishers.Sequence<[Int], Never> 타입의 publhiser를 반환합니다. 이를 따로 subscriber에게 구독 후 값을 출력해야 합니다..

번거롭습니다...

 

그렇다면 데이터를 받을 때 매번 새로 지정, 선언해야 할까요?,, No!!

  What is Subject?

 Publihsers 또는 Convenience publisher 이외에도 publisher를 커스텀해서 사용할 수 있습니다. Publihser 프로토콜을 채택해서 사용할 수도 있지만 Combine 프레임워크에서 제공된 Subject 프로토콜을 conform 한CurrentValueSubject, PassthroughSubject를  사용해서 publisher를 선언할 수 있습니다.

 

 Subject 프로토콜은 send(_:) 함수가 있습니다. publihser1의 경우 sequence에 값을 추가해서 subscriber에게 새로운 값을 전달할 수 없습니다. 반면 Subject는 send(_:)를 통해 publihser의 upstream에 값들을 추가할 수 있습니다. send(_:) 함수는 결국 subscriber에게 값을 보내는 역할을 합니다. 이때 value는 Combine oeprator가 아닌 데이터입니다! 

 

 위에서 소개했지만, Publishers.Sequence를 publihser로 선언했을 경우 제한된 sequence의 값을 전부 subscirber에게 전달한 후에 completion event를 호출합니다. 이후 publihser로써 역할은 더 이상 하지 않습니다. 하지만 데이터는 업데이트되고, 삭제도 되고, 새로 추가도 됩니다. 위에서 선언한 publisher1, 2를 사용하기에는 적합하지 않습니다. Subject 프로토콜을 구현한 CurrentValueSubject, PassthroughSubject를 사용하면 편리합니다.

  PassthroughSubject vs CurrentValueSubject. Round 1 !

 공통점은 Subject 프로토콜을 채택해 구현했다는 점입니다. send(_:)를 통해 downstream subscribers에게 값을 전달할 수 있습니다. 또한 AnyCancellable의 subscription을 hoding(유지)하고 있을 경우에 각각의 object는 cancel()되지 않습니다. 그래서 object가 diinitialize 되지 않거나 직접 cancel(), send(completion:)을 하지 않는 한 send(_:)를 통해 데이터가 추가될 경우 publish 할 수 있다는 특징이 있습니다.

 

 Subscriber를 커스텀을 해서 subscribe(_:) operator를 통해 등록할 수 있습니다. Subscription을 통해 emit 할 Output demand를 지정할 수 있습니다.

 

 send(completion:) 함수를 통해 completion event를 전달할 수 있습니다. 

 

 이 둘의 가장 큰 차이점은 각각의 타입을 갖는 object를 init 할 때 초기값을 갖고 있는지 여부입니다. 변수 선언 후 초기화할 때 PassthroughSubject는 Output generic type의 값을 갖지 않지만 CurrentValueSubject는 Output type의 초기 값을 선언해야 합니다. 

 

  CurrentValueSubject

 위에서의 초기화 관련 차이점 이외에 PassthroughSubject와 또 다른 특징이 있습니다. CurrentValueSubject는 가장 최근에 emit 한 output타입의 value를 버퍼에 저장합니다. 즉 버퍼가 있습니다!

 

 value라는 내부 변수를 갖고 있다는 특징이 있습니다. 이 값이 변하면 subscribers에게 최신으로 변한 값을 emit 합니다. CurrentValueSubject send(_:) 함수를 통해 subscribers에게 값을 전달할 때 CurrentValueSubject의 value 변수의 값 또한 downstream에게 emit 되려는 값으로 변경됩니다(send에 추가한 값). 약간 @Published 변수와 닮은 것 같기도 한 느낌이 듭니다.???!

 

 아래는 send(_:) 함수를 통해 subscribers에게 값을 전달하는 간단한 예제입니다.

let subscriptions = Set<AnyCancellable>()

let subject = CurrentValueSubject<Int,Never>(0) //1
print("---- initial value emit to subscirbers ----")
subject.sink{print($0)}.store(in: &subscriptions) //2
print("---- using send(_:) ----")
subject.send(1) //3
print("---- Line Break ----")
subject.value = 2 //4
print("DEBUG: subejct's currnet saved value: \(subject.value //5

 

//1. CurrentValueSubject의 object를 초기화할 땐 Output type. 즉 Int에 대한 초기 값을 지정해야 합니다. 초기 값을 가지고 있기 때문에 subscribers에게 자신이 가지고 있는 값을 emit 합니다.

//2. subject 변수에 sink를 통해 subscriber를 생성합니다. 이후 반환되는 AnyCancellable 타입의 return 값을 subscriptions에 저장함으로 holding 시킵니다.

//3. subscriber인 sink에게 send(_:)를 사용해 0을 전달합니다. (CurrentValueSubject를 통해!)

//4. CurrentValueSubejct의 value 변수를 변경시킵니다. value의 값이 변경됐기 때문에 subscriber에게 emit 됩니다.

//5. 현재 subejct의 변수 value가 가지고 있는 값을 출력합니다. 가장 최근에 emit 된 값을 유지하고 있습니다.

  PassthroughSubject

let subject = PassthroughSubject<Int,Never>()

 PassthroughSubejct 타입의 object를 초기화할 때 초기화 값을 넣지 않아도 됩니다. 버퍼가 없기에 가장 최근에 subscribers에게 publisher에게 publihsed 한 값을 저장하지 않습니다. 정말 말 그대로 이 변수를 통해 subscribers에게 값을 전달합니다.

  CurrentValueSubject vs PassthroughSubject. Round2!!

 PassthroughSubject 사용 시 주의할 점이 있습니다. 버퍼가 없기에 subscribers가 없으면 send(_:)를 통해 전달 한 값을 그대로 없애 버립니다. 반면 CurrentValueSubject는 최근에 send(_:) 된 값을 value를 통해 저장하고 있기 때문에 subscribers를 부착하지 않았을 때 전달한 값 중 가장 최근 값을 저장하고 있습니다. subscriber가 구독했을 때 해당 값을 전달합니다.

 

간단한 예가 있습니다.

print("---- Create CurrentValueSubject ----")
let subject3 = CurrentValueSubject<Int,Never>(-1)
subject3.value = 0
subject3.send(1)
subject3
    .sink {
        print("DEBUG: subejct3 \($0)")
    } receiveValue: {
        print("received value: \($0)")
    }.store(in: &subscriptions)
subject3.send(2)
subject3.send(completion: .finished)

 CurrentValueSubject 타입의 subejct3 변수에 아무런 subscribers가 없을 때 send(_:)를 통해 값을 보낸 결과입니다.

 위에서 설명한 대로 subscriber가 없어도 가장 최근에 send 한 값 1을 subject3.value에 저장하고 있기 때문에 subscriber에게 value 값을 전달합니다.

print("---- Create PassthroughSubject ----")
let subject2 = PassthroughSubject<Int,Never>()
subject2.send(0)
subject2
    .sink {
        print("DEBUG: subejct2 \($0)")
    } receiveValue: {
        print("received value: \($0)")
    }.store(in: &subscriptions)
subject2.send(2)
subject2.send(completion: .finished)

PassthroughSubject 타입의 subject2의 경우입니다. 

subscribers가 없을 경우 또는 subscriber의 demand가 0일 경우에 send(_:)를 통해 전달받은 값을 버려버립니다...

 

  CurrentValueSubject 사용 예

 간단하게 맨 처음 결과 1 사진처럼,  Publishers.Sequence를 통한 [1,2,3,4,5,6]. publisher의 값 전달을 확인했었습니다. 이번에는 CurrentValueSubject를 사용할 경우 어떤 차이가 있는지 소개하려고 합니다. Publishers.Sequence publhiser는 Elements의 값을 subscriber에게 전달하면 completion event를 발생한다는 점 잊지 않으셔야 합니다!

 

 CurrentValueSubject 내부 변수 value를 통해 버퍼에 초기값을 갖고 있는다는 특징이 있습니다. 이를 통해 [1,2,3,4,5,6]을 저장하고 한 개씩 subscriber에게 전달하려고 합니다.

let publisher6 = CurrentValueSubject<[Int],Never>([1,2,3,4,5,6])

우선 초기값으로 배열을 값으로 갖도록 선언합니다.

publisher6
.sink {
    print("DEBUG: Publihser6 state: \($0)")
} receiveValue: {
    print("received value: \($0)")
}.store(in: &subscriptions)

호호. 간단합니다. 이제 subscirber를 연결하면

 

출력 값은?

 

.

.

 배열이 통째로 전달되게 됩니다.. Output type를 Int로 바꾸고 send(_:)를 써서 1,2,3,4,5,6을 subscription에게 보내면 subscriber에게 값을 각각 주면 각각의 값이 전달되긴 할 것 같습니다... 다른 방법이 없을까요?

 

 Operator를 사용하면 됩니다. 많으면서도 은근히 적다고 느껴지는?(다양합니다.) Operator. flatMap(_:) Operator를 사용해 upstream publisher의 타입을 바꿀 것입니다. 참고로 compactMap은 sequence published값 중 non-nil의 값을 sequence로 반환한다고 생각하시면,,(저도 둘의 차이점이 살짝 했갈리긴 하지만 다릅니다.)

 

publisher6에 sink를 달기 전에. flatMap {$0.publihser}를 chain으로 달면 결과는 달라지게 됩니다. //compactMap(_:)은 안됩니다.

 위에서 언급한 Publihsers.Sequence의 pubhliser와 CurrentValueSubject가 다른 점이 있습니다. 전자의 경우 현재 갖고 있는 Sequence의 element를 전부 subscriber에게 전달할 경우 바로 completion event를 호출한다는 점입니다. 더 이상 값 추가 해도 새로운 publhiser만 반환할 뿐, publish 되지 않습니다. 

 

 반면 CurrentValueSubect는 subscription을 (holding) 계속 가지고 있는 경우 publihser는 completion event를 호출하지 않는다는 점입니다. 그렇다는 것은 추가로 CurrentValueSubject타입의 object에게 send(_:)로 값을 전달한다면 subscriber는 값을 받을 수 있음을 의미합니다. 반대로 subscriber는 구독했지만, cancellable타입의 subscription을 holding 하지 않을 경우 value의 값을 방출한 이후에 cancel() 됩니다.

 

Cancellable까지 정리하려고 했는데 글이 길어졌네요.. 다음번에 다루려고 합니다.

 

추가로 PassthroughSubject는 초기 값을 저장할 수 없음으로 send(_:)를 통해 배열을 보내야 합니다.

 

긴 글 읽어주셔서 감사합니다.

틀린 내용 발견 시 댓글로 남겨주시면 정말 감사합니다!!! (항상 공부 중입니다. 정확한 내용을 환영합니다:)

 

참고

https://www.kodeco.com/books/combine-asynchronous-programming-with-swift

 

Combine: Asynchronous Programming with Swift

Master declarative asynchronous programming with Swift using the Combine framework! Writing asynchronous code can be challenging, with a variety of possible interfaces to represent, perform, and consume asynchronous work — delegates, notification center,

www.kodeco.com

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

728x90