포스트

Subject 종류 별로 알아보기

Subject

Subject는 항목을 방출만 하는 Obsrvable과는 다르게, ObservableObserver의 역할을 동시에 수행하는 클래스입니다. 다시 말해, 다른 Observable로부터 항목을 전달받을 수도 있고, 이렇게 전달받은 항목을 다른 Observer에게 방출할 수 있습니다. 이런 이유로 Subject는 프록시(Proxy) 혹은 브릿지(Bridge) 옵저버블이라고도 불립니다.

Subject의 작동 방식을 도식화해보면 아래와 같습니다.

2

SubjectObserver이기에 하나 혹은 그 이상의 Observable을 구독할 수 있습니다. 동시에 Observable이기에 항목을 Observer에게 재방출하거나, 새로운 항목을 방출하도록 할 수 있습니다. 이러한 특징 덕분에, 구독을 하게 되면 가지고 있는 항목을 모두 방출해야 하는 Observable과 달리 Subject는 런-타임 도중 필요할 때마다 Subject에 항목을 전달하고, 구독한 Observable에게 항목을 방출하도록 할 수 있습니다.

SubjectObservable과 마찬가지로 Next, ErrorCompleted 항목을 모두 방출할 수 있습니다. Next 항목을 방출하면 값이 전달되고, ErrorCompleted 항목을 방출하면 스트림이 중단되고, 새로운 항목을 방출한다 하더라도 구독자에게 전달되지 않습니다.

SubjectPublishSubject, BehaviorSubject, ReplaySubjectAsyncSubject가 있으며, 각 Subject는 구독 이전에 방출한 항목을 어떻게 처리하는지 차이만 있을 뿐, 기본적인 원리는 모두 동일합니다.

PublishSubject

3

PublishSubjectObserver가 구독을 한 이후 시점부터 소스 옵저버블이 Observer에게 방출한 항목을 전달합니다. 즉, Observer는 소스 옵저버블이 구독 이전 시점에 방출한 항목은 전달받을 수 없습니다(무시됩니다). 아래 예제는 PublishSubject의 작동 방식을 보여줍니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let disposeBag = DisposeBag()

let publishSubject = PublishSubject<Int>()

// 아무런 구독자가 없기에 해당 이벤트는 무시됩니다.
publishSubject.onNext(10)

publishSubject
    .subscribe { print($0) }
    .disposed(by: disposeBag)

publishSubject.onNext(20)

publishSubject.onCompleted()
// Completed 항목을 방출했기에 해당 이벤트는 무시됩니다.
publishSubject.onNext(30)

PublishSubjectBehaviorSubject와 다르게 초기 값이 필요 없습니다. 6번째 줄에 10이 담긴 Next 항목을 방출했으나, 해당 시점에는 아직 아무런 구독자가 없기에 값이 구독자에게 전달되지 않습니다. 9번째 줄에서 해당 서브젝트를 구독을 하였고, 이후 방출하는 Next 항목이 정상적으로 구독자에게 전달되고 있습니다.

BehaviorSubject

4

BehaviorSubjectObserver가 구독을 하게 되면 소스 옵저버블이 최근에 방출한 항목이나 서브젝트 선언 시 전달한 기본 값을 Observer에게 전달합나다. 이후 소스 옵저버블이 항목을 방출하면 (PublishSubject와 동일하게) 항목을 전달받을 수 있습니다. 아래 예제는 BehaviorSubject의 작동 방식을 보여줍니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let behaviorSubject = BehaviorSubject<Int>(value: 10)

behaviorSubject
    .subscribe { print("Sub1 - ", $0) }
    .disposed(by: disposeBag)

behaviorSubject.onNext(20)

behaviorSubject
    .subscribe { print("Sub2 - ", $0) }
    .disposed(by: disposeBag)

behaviorSubject.onNext(30)
behaviorSubject.onCompleted()

BehaviorSubjectPublishSubject와 다르게 초기 값이 필요합니다. 새로운 구독자가 추가되면 선언 시 전달한 초기 값이 Next 항목으로 전달됩니다. 4번째 줄에 새로운 구독을 하게 되면 선언 시 전달한 초기 값인 10이 구독자에게 곧바로 전달됩니다. 그리고 7번째 줄에서 20이 담긴 Next 항목을 방출하였습니다. 다시 새로운 구독을 하게 되면 마지막으로 방출한 항목인 20이 새로운 구독자에게 곧바로 전달됩니다.

ReplaySubject

5

ReplaySubject는 구독 시점에 상관없이 소스 옵저버블이 구독 이전 시점에 방출한 (버퍼에 저장된) 항목을 Observer에게 전달합니다. 아래 예제는 ReplaySubject의 작동 방식을 보여줍니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let replaySubject = ReplaySubject<Int>.create(bufferSize: 3)

(1...10).forEach { replaySubject.onNext($0) }

replaySubject
    .subscribe { print("Sub1 - ", $0) }
    .disposed(by: disposeBag)

replaySubject.onNext(11)

replaySubject
    .subscribe { print("Sub2 - ", $0) }
    .disposed(by: disposeBag)

replaySubject.onCompleted()

ReplaySubject는 선언 시 버퍼 사이즈가 필요합니다. 방출된 Next 항목을 차례로 버퍼에 저장해두고, 새로운 구독이 발생하면 버퍼에 저장된 항목을 구독자에게 전달합니다. 3번째 줄에 1부터 10까지 담긴 Next 항목을 방출했습니다. 그러면 버퍼에는 8, 9, 10 항목이 저장되게 되고, 새로운 구독이 발생하면 버퍼에 저장된 해당 항목이 새로운 구독자에게 곧바로 전달됩니다. 9번째 줄에 새로운 Next 항목을 방출하면, 버퍼도 달라지게 됩니다. 가장 나중에 버퍼에 들어온 항목이 비워지고, 새로운 항목이 채워지게 됩니다. 따라서, 12번째 줄에서 새로운 구독을 하게 되면 9, 10, 11 항목이 새로운 구독자에게 전달됩니다.

AsyncSubject

6

AsyncSubject는 소스 옵저버블이 Next 항목을 방출하더라도 Observer에게 항목을 전달하지 않고, Completed 항목을 방출하면 마지막으로 방출한 항목을 전달합니다. 아래 예제는 AsyncSubject의 작동 방식을 보여줍니다.

1
2
3
4
5
6
7
8
9
10
let asyncSubject = AsyncSubject<Int>()

asyncSubject
    .subscribe { print($0) }
    .disposed(by: disposeBag)

asyncSubject.onNext(10)
asyncSubject.onNext(20)
asyncSubject.onNext(30)
asyncSubject.onCompleted()

4번째 줄에서 새로운 구독을 했음에도 불구하고 7~9번째 줄에 Next 항목을 차례로 방출해도 구독자에게 전달되지 않습니다. 10번째 줄에서 Completed 항목을 방출해야 비로소 마지막으로 방출한 값인 30이 구독자에게 전달됩니다.

Multicast

7

SubjectObservable과 다른 결정적인 차이가 하나 존재합니다. ObservableObserver와의 관계가 유니캐스트인 반면에, SubjectObserver와의 관계가 멀티캐스트입니다.

유니캐스트Observable이 각 Observer와의 구독 관계가 독립적이라는 의미입니다. Observable의 항목은 Observer가 구독을 하게 되면 비로소 생성되고 방출됩니다. 그 전까지는 일종의 청사진에 불과합니다. ObserverObservable을 구독하게 되면 Observable은 청사진을 바탕으로 항목을 Observer에게 방출하고, 이렇게 방출하는 항목은 서로 영향을 주지 않습니다. 아래 예제는 유니캐스트가 일어나는 일반적인 상황을 보여줍니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let observable = Observable<Int>.create { observer in
    let random = Int.random(in: 1...100)
    observer.onNext(random)
    return Disposables.create()
}

observable
    .subscribe { print($0) }
    .disposed(by: disposeBag)

observable
    .subscribe { print($0) }
    .disposed(by: disposeBag)

observable
    .subscribe { print($0) }
    .disposed(by: disposeBag)

위 예제에서는 observable이 1에서 100 사이 무작위 숫자를 항목으로 방출하고 있습니다. 새로운 구독을 할 때마다 방출되는 무작위 숫자가 달라집니다. 이는 Observable이 모든 Observer에게 각 각 항목을 전달하고 있다는 의미입니다.

멀티캐스트는 소스 옵저버블이 방출하는 항목은 모든 Observer와 공유한다는 의미입니다. 새롭게 구독을 하면 새로운 관계가 형성되는 유니캐스트와는 다르게, 멀티캐스트는 그저 소스 옵저버블을 바라보는 Observer가 하나 느는 것에 불과합니다. (구독 이전에 방출한 항목을 처리하는 방식만 다를 뿐) 기본적으로 Subject멀티캐스트 방식으로 동작합니다. 아래 예제는 멀티캐스트가 일어나는 일반적인 상황을 보여줍니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let publishSubject = PublishSubject<Int>()

publishSubject
    .subscribe { print($0) }
    .disposed(by: disposeBag)

publishSubject
    .subscribe { print($0) }
    .disposed(by: disposeBag)

publishSubject
    .subscribe { print($0) }
    .disposed(by: disposeBag)

let random = Int.random(in: 1...100)
publishSubject.onNext(random)
publishSubject.onCompleted()

publishSubject가 1에서 100 사이 무작위 숫자를 항목으로 방출하고 있습니다. 모든 Observer가 동일한 무작위 숫자를 받아 출력합니다. 이는 소스 옵저버블이 모든 Observer에게 한번만 항목을 방출하고 있다는 의미입니다. 구독을 잘 공유하고 있다는 거죠.

언제 사용하나요?

Subject의 개념은 그리 어렵지 않습니다. 하지만, 그 동안 저를 줄곧 괴롭혀 왔던 점 중 하나는 ‘언제 어떻게 사용하는지’였습니다. 기본적으로 Subject는 런-타임 중 항목을 전달해야 할 필요가 있을 때 사용합니다.

삐삐 프로젝트에서 주 사용 사례 중 하나는 ‘A 뷰 컨트롤러에서 생성된 데이터를 B 뷰 컨트롤러에 전달해줄 필요가 있을 때’ 였습니다. 삐삐 프로젝트에서는 적절한 범위의 캘린더를 만들어주기 위해 FamilyCreatedAt이라는 값이 꼭 필요합니다. 그렇지 않으면 캘린더의 시작 지점을 알 수 없기 때문이죠. 이 값은 스플래시 화면에서 미리 서버와 통신해 받은 값을 따로 저장해두고, 캘린더 화면이 보여질 때 활용해 캘린더를 만듭니다.

아래 예제는 이러한 과정을 보여줍니다.1

1
2
3
public class Repository: RxObject {
    public let familyCreatedAt = BehaviorSubject<String?>(value: nil)
}
1
2
3
4
5
6
7
8
9
10
class SplashViewController {
    
    func bind() {
        // 통신한 결과인 familyCreatedAt 값을 Repository로 방출함
        viewModel.output.familyCreatedAt
            .subscribe { date in repository.familyCreatedAt.onNext(date) }
            .disposed(by: disposeBag)
    }

}
1
2
3
4
5
6
7
8
9
10
11
class CalendarViewController {

    override func viewWillAppear(_ animated: Bool) {
        // 뷰가 보여지면 통신한 결과인 familyCreatedAt 값을 받아와 캘린더를 만듦
        repository.familyCreatedAt
            .map { Reactor.Action.addCalendarItem($0) }
            .bind(to: reactor.action)
            .disposed(by: disposeBag)
    }

}

일반적으로 다른 뷰 컨트롤러에 데이터를 전달해주려면 클로저나 델리게이트 패턴을 주로 사용해야 했습니다. 이는 코드를 복잡하게 만들고, 유연성도 떨어집니다. 하지만, RxSwift를 활용하면 이 과정이 더욱 간단해집니다. (Rx가 없었다면 어떻게 코드를 짜야 했을까요?) 이 뿐만 아니라 다른 활용법도 무궁무진하겠지만, 겪은 사례로만 설명드렸습니다.

참고 자료


  1. 쉬운 이해를 위해 뷰 컨트롤러에서 모든 로직이 일어나는 것마냥 코드를 작성했지만, 실제 프로젝트에서는 뷰 모델에서 항목을 방출하고 전달받습니다. 

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.