Subject 종류 별로 알아보기
Subject
Subject
는 항목을 방출만 하는 Obsrvable
과는 다르게, Observable
과 Observer
의 역할을 동시에 수행하는 클래스입니다. 다시 말해, 다른 Observable
로부터 항목을 전달받을 수도 있고, 이렇게 전달받은 항목을 다른 Observer
에게 방출할 수 있습니다. 이런 이유로 Subject
는 프록시(Proxy) 혹은 브릿지(Bridge) 옵저버블이라고도 불립니다.
Subject
의 작동 방식을 도식화해보면 아래와 같습니다.
Subject
는 Observer
이기에 하나 혹은 그 이상의 Observable
을 구독할 수 있습니다. 동시에 Observable
이기에 항목을 Observer
에게 재방출하거나, 새로운 항목을 방출하도록 할 수 있습니다. 이러한 특징 덕분에, 구독을 하게 되면 가지고 있는 항목을 모두 방출해야 하는 Observable
과 달리 Subject
는 런-타임 도중 필요할 때마다 Subject
에 항목을 전달하고, 구독한 Observable
에게 항목을 방출하도록 할 수 있습니다.
Subject
는 Observable
과 마찬가지로 Next
, Error
와 Completed
항목을 모두 방출할 수 있습니다. Next
항목을 방출하면 값이 전달되고, Error
나 Completed
항목을 방출하면 스트림이 중단되고, 새로운 항목을 방출한다 하더라도 구독자에게 전달되지 않습니다.
Subject
는 PublishSubject
, BehaviorSubject
, ReplaySubject
와 AsyncSubject
가 있으며, 각 Subject
는 구독 이전에 방출한 항목을 어떻게 처리하는지 차이만 있을 뿐, 기본적인 원리는 모두 동일합니다.
PublishSubject
PublishSubject
는 Observer
가 구독을 한 이후 시점부터 소스 옵저버블이 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)
PublishSubject
는 BehaviorSubject
와 다르게 초기 값이 필요 없습니다. 6번째 줄에 10
이 담긴 Next
항목을 방출했으나, 해당 시점에는 아직 아무런 구독자가 없기에 값이 구독자에게 전달되지 않습니다. 9번째 줄에서 해당 서브젝트를 구독을 하였고, 이후 방출하는 Next
항목이 정상적으로 구독자에게 전달되고 있습니다.
BehaviorSubject
BehaviorSubject
는 Observer
가 구독을 하게 되면 소스 옵저버블이 최근에 방출한 항목이나 서브젝트 선언 시 전달한 기본 값을 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()
BehaviorSubject
는 PublishSubject
와 다르게 초기 값이 필요합니다. 새로운 구독자가 추가되면 선언 시 전달한 초기 값이 Next
항목으로 전달됩니다. 4번째 줄에 새로운 구독을 하게 되면 선언 시 전달한 초기 값인 10
이 구독자에게 곧바로 전달됩니다. 그리고 7번째 줄에서 20
이 담긴 Next
항목을 방출하였습니다. 다시 새로운 구독을 하게 되면 마지막으로 방출한 항목인 20
이 새로운 구독자에게 곧바로 전달됩니다.
ReplaySubject
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
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
Subject
는 Observable
과 다른 결정적인 차이가 하나 존재합니다. Observable
은 Observer
와의 관계가 유니캐스트
인 반면에, Subject
는 Observer
와의 관계가 멀티캐스트
입니다.
유니캐스트
는 Observable
이 각 Observer
와의 구독 관계가 독립적이라는 의미입니다. Observable
의 항목은 Observer
가 구독을 하게 되면 비로소 생성되고 방출됩니다. 그 전까지는 일종의 청사진에 불과합니다. Observer
가 Observable
을 구독하게 되면 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가 없었다면 어떻게 코드를 짜야 했을까요?) 이 뿐만 아니라 다른 활용법도 무궁무진하겠지만, 겪은 사례로만 설명드렸습니다.
참고 자료
쉬운 이해를 위해 뷰 컨트롤러에서 모든 로직이 일어나는 것마냥 코드를 작성했지만, 실제 프로젝트에서는 뷰 모델에서 항목을 방출하고 전달받습니다. ↩