티스토리 뷰

반응형

안녕하세요 Pingu입니다.🐧

 

지난 글에서는 Apple에서 미리 정의해둔 Publisher들을 알아봤었는데, 이번 글에서는 이어서 Publisher 프로토콜을 채택하는 또 다른 녀석들인 Subject들에 대해서 알아보려고 합니다.

 

Subject

일단 Subject의 정의를 볼까요?

Subject도 프로토콜입니다. Publisher를 채택한 프로토콜이네요.

밑에 설명에 보면 "Subject는 stream에 send(_:) 메서드를 호출해서 값을 주입할 수 있는 Publisher이다."라고 적혀있네요. 그래서 기존에 Combine을 사용하지 않던 코드에 Combine 모델을 적용하고 싶을 때 사용하면 좋다고 합니다.

 

대충 뭔지는 알겠으니 Subject 프로토콜의 구현을 좀 더 살펴볼게요.

@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
public protocol Subject : AnyObject, Publisher {
    func send(_ value: Self.Output)
    func send(completion: Subscribers.Completion<Self.Failure>)
    func send(subscription: Subscription)
}

extension Subject Where Self.Output == Void {
    public func send()
}

이렇게 총 3개의 필수적인 send 메서드와 Output이 Void일 때 필요한 send() 메서드가 구현되어있습니다.

3개의 필수적인 send 메서드를 보면 value, completion, subscription을 보내기 위한 것들 같네요!

참고로 여기서 값들을 보내지는 곳은 Subscriber 입니다.

 

그럼 Subject도 프로토콜이니까 Apple이 이걸 채택한 뭔가를 만들어 놨겠죠??

그걸 알아보면 다음과 같습니다.

  • CurrentValueSubject
    • 단일 값을 래핑하고 값이 변경할 때마다 새로운 값을 내보내는 Subject
  • PassthroughSubject
    • Downstream Subscriber에게 값을 전파하는 Subject

일단 정의는 위와 같은데 하나씩 자세히 알아보도록 하겠습니다.

 

CurrentValueSubject

CurrentValueSubject 정의를 먼저 보겠습니다.

설명에 보면 아직 자세히 알아보지 않은 PassthroughSubject와 비교하는 게 나오네요.

 

CurrentValueSubject는 PassthroughSubject와 다르게 가장 최근에 published 된 값의 버퍼를 유지한다고 합니다.

그리고 CurrentValueSubject의 send(_:)를 호출하면 현재 값도 업데이트돼서 값을 직접 업데이트하는 거랑 동일한 효과를 얻을 수 있다고 하네요.

 

현재 값을 저장할 공간이 필요하니 왠지 구현에 그런 공간이 존재할 거 같은데, 정말 있을지 구현을 확인해볼게요.

@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
final public class CurrentValueSubject<Output, Failure> : Subject where Failure : Error {
    final public var value: Output
    
    public init(_ value: Output)

    final public func receive<S>(subscriber: S) where Output == S.Input, Failure == S.Failure, S : Subscriber
    
    final public func send(subscription: Subscription)
    final public func send(_ input: Output)
    final public func send(completion: Subscribers.Completion<Failure>)
}

CurrentValueSubject의 구현을 보면 Publisher, Subject에 필요한 메서드들이 존재하고, 예상대로 최신 값을 저장하기 위한 value라는 프로퍼티가 있는 것을 볼 수 있습니다.

 

그리고 생성자를 보면 최신값을 받도록 되어있는데요, 즉 CurrentValueSubject에 값이 없을 수는 없나 봅니다.

 

그럼 정의도 봤고 구현도 봤으니 간단하게 사용해보겠습니다~!

let currentValueSubject = CurrentValueSubject<String, Never>("Pingu 첫번째 값")
currentValueSubject
    .sink(receiveCompletion: { print("1 번째 sink completion: \($0)") },
          receiveValue: { print("1 번째 sink value: \($0)") })

currentValueSubject
    .sink(receiveCompletion: { print("2 번째 sink completion: \($0)") },
          receiveValue: { print("2 번째 sink value: \($0)") })
    

currentValueSubject
    .sink(receiveCompletion: { print("3 번째 sink completion: \($0)") },
          receiveValue: { print("3 번째 sink value: \($0)") })

// 현재 Subscriber들에게 모두 보냄
currentValueSubject.send("Pingu 두번째 값")
currentValueSubject.send(completion: .finished)

print(currentValueSubject.value)

순서가 보장되는지 확인하려고 3개의 Subscriber를 만들었는데요, 테스트해보니 send가 subscriber에 도착하는 순서는 딱히 보장되지 않는 거 같습니다.

 

뭐 어쨌든 결과는 아래와 같습니다.

1 번째 sink value: Pingu 첫번째 값
2 번째 sink value: Pingu 첫번째 값
3 번째 sink value: Pingu 첫번째 값
3 번째 sink value: Pingu 두번째 값
1 번째 sink value: Pingu 두번째 값
2 번째 sink value: Pingu 두번째 값
3 번째 sink completion: finished
1 번째 sink completion: finished
2 번째 sink completion: finished
Pingu 두번째 값

실제로 사용해보면 알 수 있는 것은 자신은 subsribe 하는 모든 subscriber에게 send를 통해서 값을 보낸다는 점과 send메서드를 호출하면 CurrentValueSubject의 value 프로퍼티도 업데이트된다는 사실!입니다. 따라서 CurrentValueSubject에는 최신 값만 유지하고 있게 됩니다.

 

그럼 만약에 subscription을 취소하면 어떻게 될까요? 뭐 당연하게도 값이 전달되지 않겠죠?

let currentValueSubject = CurrentValueSubject<String, Never>("Pingu 첫번째 값")

let firstSubscription = currentValueSubject
    .sink(receiveCompletion: { print("1 번째 sink completion: \($0)") },
          receiveValue: { print("1 번째 sink value: \($0)") })

let secondSubscription = currentValueSubject
    .sink(receiveCompletion: { print("2 번째 sink completion: \($0)") },
          receiveValue: { print("2 번째 sink value: \($0)") })
    .cancel()


// 현재 Subscriber들에게 모두 보냄
currentValueSubject.send("Pingu 두번째 값")
currentValueSubject.send(completion: .finished)

print(currentValueSubject.value)

위와 같이 2번째 secondSubscription은 cancel()을 사용해서 취소해봤습니다.

그럼 결과는 예상대로..

1 번째 sink value: Pingu 첫번째 값
2 번째 sink value: Pingu 첫번째 값
1 번째 sink value: Pingu 두번째 값
1 번째 sink completion: finished
Pingu 두번째 값

이렇게 2번째 sink에서 만들어진 Subscription에는 두 번째 값이 전달되지 않습니다.

 

그럼 여기서 completion은 어떤 역할을 할까요?

두 개의 subscription을 만들고 하나는 먼저 finished completion을 보내보겠습니다.

let currentValueSubject = CurrentValueSubject<String, Never>("Pingu 첫번째 값")

let firstSubscription = currentValueSubject
    .sink(receiveCompletion: { print("1 번째 sink completion: \($0)") },
          receiveValue: { print("1 번째 sink value: \($0)") })

currentValueSubject.send(completion: .finished)

let secondSubscription = currentValueSubject
    .sink(receiveCompletion: { print("2 번째 sink completion: \($0)") },
          receiveValue: { print("2 번째 sink value: \($0)") })


// 현재 Subscriber들에게 모두 보냄
currentValueSubject.send("Pingu 두번째 값")

print(currentValueSubject.value)

위와 같이 코드를 작성하면 Subscription들에게 "Pingu 두 번째 값"이 전달될까요?

실행해서 결과를 보면 다음과 같습니다.

========CurrentValueSubject========
1 번째 sink value: Pingu 첫번째 값
1 번째 sink completion: finished
2 번째 sink completion: finished
Pingu 첫번째 값

이렇게 finished를 전달한 뒤에 send를 통해 보내지는 값들은 모두 무시되며 CurrentValueSubject의 value도 업데이트되지 않습니다.

또한 2번째 subscription에도 finished가 전달되는 것도 볼 수 있습니다.

PassthroughSubject

그럼 다음으로 PassthroughSubject에 대해서 알아보겠습니다.

PassthroughSubject의 정의를 먼저 볼게요.

정의를 보니 "downstream의 subscriber들에게 값을 전파한다"라고 되어있네요.

그리고 아까 알아본 CurrentValuSubject와 다르게 생성할 때 딱히 초기값이 필요하지 않다고 합니다.

또한 최신 값을 저장하기 위한 공간도 필요 없죠.

이름에서 느낄 수 있듯이 그냥 값을 스쳐 보내는 쿨한 녀석입니다.

따라서 만약에 subscriber가 없거나 Demand가 0이라면 값을 보내더라도 아무 일도 발생하지 않게 됩니다.

 

구현을 보면 딱 Subject 프로토콜에 필요한 메서드들만 정의되어있습니다.

@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
final public class PassthroughSubject<Output, Failure> : Subject where Failure : Error {

    public init()
    
    final public func receive<S>(subscriber: S) where Output == S.Input, Failure == S.Failure, S : Subscriber
    
    final public func send(subscription: Subscription)
    final public func send(_ input: Output)
    final public func send(completion: Subscribers.Completion<Failure>)
}

 

아까 CurrentValueSubject와 사용법은 동일하며, 최신 값을 저장하는 프로퍼티가 없어서 최신 값에 접근하지 못합니다.

let passthroughSubject = PassthroughSubject<String, Never>()

let firstSubscription = passthroughSubject
    .sink(receiveCompletion: { print("1번째 sink completion: \($0)") },
          receiveValue: { print("1번째 sink value: \($0)")}
    )

passthroughSubject.send("PassthroughSubject 1번째 값")

let secondSubscription = passthroughSubject
    .sink(receiveCompletion: { print("2번째 sink completion: \($0)") },
          receiveValue: { print("2번째 sink value: \($0)")}
    )

passthroughSubject.send("PassthroughSubject 2번째 값")

위와 같이 간단하게 사용해볼 수 있는데요, 아까와 마찬가지로 second Subbscription은 생성되기 전에 내보내진 값인 "PassthroughSubject 1번째 값"은 받지 못합니다.

 

끝입니다.😊 최신 값을 저장하지 못한다는 것 외에는 다른 게 없어요.

 

이렇게 Subject 프로토콜과 Apple에서 미리 구현해둔 Subject에 대해 알아봤습니다.

Subject는 Combine을 사용하지 않던 코드에서 Combine을 사용하고자 할 때 유용하며, 미리 구현된 Subject에는 CurrentValueSubject, PassthroughSubject가 있고 두 개의 차이점은 최신 값의 저장 유무였습니다.

 

미리 구현된 Publisher들에 비해 개수가 적고 단순한 거 같네요.

 

다음 글에서는 Subscriber 사용을 위해 미리 정의된 것들에 대해 알아보도록 하겠습니다.

 

전체 코드는 여기에서 볼 수 있습니다.

 

감사합니다.

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함