Subject란 무엇일까?
Subject는 Observable과 Obsever 둘다 할 수 있다.
여기서 Observable은 관찰 대상이다. Observable은 관찰 가능한 흐름을 생성하고 Observer가 Observable을 구독하여 이벤를 처리하게 된다. Subject는 이 둘을 다 하는 타입으로 관찰가능한 흐름을 생성하고 이벤트를 방출시키며 알맞는 이벤트를 처리하게 된다.
Subject 타입
- PublishSubject : 어떠한 값도 없이 생성되며 오직 새로운 구독자에게 elements를 방출한다.
- BehaviorSubject : 초기값과 함께 생성되며 이 생성된 값을 구독자에게 재생시키고 최신의 elements을 재생시킨다.
- ReplaySubject : 버퍼사이즈와 함께 생성. elements의 버퍼사이즈 또한 유지되며 새로운 구독자에게 재생 시킨다.
- AsyncSubject : 시퀀스에서 가장 최신의 .next 이벤트만 방출되며 구독자가 .completed 이벤트를 받은 경우에만 발생한다
- PublishRelay & BehaviorRelay : subject를 wrap 하고 있는 type. .next 이벤트만 받음. relay에 .completed, .error 이벤트를 추가할 수 없음 -> 종료되지 않는 시퀀스 Relay는 다음 이 시간에 ...
(지금은 무슨말인지 모르겠지만 실행해 보면 알게 되는 것 같다. 일단 적음)
PublishSubject example
let disposeBag = DisposeBag()
DisposeBag을 생성. DisposeBag은 dispose 되어야 하는 Observable를 모아두었다가 ViewController 메모리 해제시 한번에 메모리를 해제할 수 있게 관리해준다. 혹은 dispose 메소드를 호출하여 수동으로 관리해 줘도 무관하지만 꼭 모든 이벤트 방출과 동작이 끝나면 dispose 하는 것을 잊지 말아야 함. 메모리 누수가 일어 날 수도 있음
ViewController에서 Controls 객체는 지속적으로 옵저빙 해야 하기 때문에 수동으로 dispose 안하고 ViewController의 생명주기를 따르기 위해 DisposeBag을 사용하는 것 같다.
let subject = PublishSubject<String>()
subject.onNext("1")
이때 subject로 생성한 Observable이 .next 이벤트를 방출하더라고 Subscriber가 없으므로 이에 맞는 동작을 수행하지 않는다.
let subscriptionOne = subject
.subscribe(onNext: {
print("첫번째 구독 : \($0)")
})
subject.onNext("2")
subscriber해준다. 이후 .next이벤트를 방출하면 subscriber가 있으므로 첫번째 subscriber가 .next 이벤트를 받아 프린트 하는 동작을 수행하게 된다.
let subscriptionTwo = subject.subscribe(onNext: {
print("두번째 구독 : \($0)")
})
subject.onNext("3")
이 상태에서는 첫번째와 두번째 subscriber 가 모두 subscription 되어 있으므로 onNext이벤트를 받을 수 있다.
subscriptionOne.dispose()
subject.onNext("4")
첫번째 구독을 dispose 해주면 구독을 종료하므로 .next 이벤트를 방출하면 두번째 subscriber만 이벤트를 받아 동작을 수행할 수 있음
subject.onCompleted()
.completed 이벤트를 방출하였으므로 즉시 terminate [종료] 된다. 이후에 .next 이벤트를 방출하면 .completed 이벤트가 발생한다.
새롭게 subscribe 하고 .next로 이벤트를 발생 시켜도 .completed 이벤트가 발생한다
Behavior Subject
enum TestError: Error {
case error
}
.error 이벤트가 발생할 때 사용될 enum을 추가
let subject = BehaviorSubject(value: "initial value")
let disposeBag = DisposeBag()
BehaviorSubject는 구독 후 가장 마지막 값을 방출하기 때문에 반드시 초기 값이 필요함. 만약 초기값이 필요없는 Subject를 사용하고 싶은 경우 위에 PublishSubject를 사용함.
subject.subscribe { event in
print("첫 번째 subscribe : ", event.element ?? event)
}.disposed(by: disposeBag)
subject.onNext("1")
BehaviorSubject 를 구독하고 .next 이벤트를 방출하면 BehaviorSubject의 초기값 (initial value)와 .next 이벤트로 넘겨준 값이 함께 콘솔에 찍히는 것을 알 수 있다.
첫 번째 subscribe : initial value
첫 번째 subscribe : 1
subject.onError(TestError.error)
.error 이벤트를 방출하면 BehaviorSubject는 즉시 terminate(종료)되어 다시 빌드하면
첫 번째 subscribe : initial value
첫 번째 subscribe : 1
첫 번째 subscribe : error(error)
subscriber가 .error 이벤트를 받은 것을 알 수 있다.
subject.subscribe { event in
print("두 번째 subscribe : ", event.element ?? event)
}.disposed(by: disposeBag)
새로 subscriber를 등록. BehaviorSubject는 가장 최신의 이벤트를 받기 때문에 두 번째 Subscriber는 .error 이벤트를 즉시 받게 된다. (.error 이벤트 방출 -> 두 번째 subscribe 등록 순서 임)
첫 번째 subscribe : initial value
첫 번째 subscribe : 1
첫 번째 subscribe : error(error)
두 번째 subscribe : error(error)
그림으로 그려보면
즉시 가장 최근 방출된 이벤트를 수행한다는 것을 생각하면 쉽다.
시간에 민감한 앱을 만들때 사용할 수 있다. completed 되고 나면 다음 subscriber에 대해서 종료되었다는 이벤트를 방출하기는 한다
ReplaySubject
ReplaySubject를 사용하면 메모리에 버퍼 저장 공간을 필요로 하게 된다. 어떻게 구현되고 동작하는지 보면 쉽게 알 수 있다.
⭐️ replay를 위한 메모리 공간이 필요하므로 메모리 용량을 많이 차지하는 이미지와 같은 인스턴스를 사용할 경우 주의가 필요하다.
let subject = ReplaySubject<Int>.create(bufferSize: 2)
let disposeBag = DisposeBag()
ReplaySubject를 생성할 때 create 메소드를 사용해서 버퍼 크기를 지정할 수 있다. 이때 버퍼 사이즈가 가리키는 것은 Subject가 방출하는 이벤트의 갯수라고 생각하면 쉽다.
subject.onNext(1)
subject.onNext(2)
subject.onNext(3)
next 이벤트로 element를 세번 방출하면 버퍼 공간을 2로 설정해 두었기 때문에 2번째와 3번째 이벤트가 버퍼에 저장되게 된다. (버퍼는 queue or FIFO 입니당)
subject.subscribe { event in
print("첫 번째 subscriber : ", event.element ?? event)
}.disposed(by: disposeBag)
Subject를 Subscription 함. ReplaySubject는 명칭의 Replay 처럼 버퍼에 있는 element를 수행합니다.
첫 번째 subscriber : 2
첫 번째 subscriber : 3
.next 이벤트는 이전에 방출되었고 그 후에 Subscription 되었는데 버퍼에 있던 Element 가 찍힌 것을 볼 수 있습니다
그렇다면 여기에 새로운 subscriber를 등록한다면? 첫 번째와 마찬가지로 두번째와 세번째 이벤트를 수행하는 것을 확인 할 수 있음
subject.subscribe { event in
print("두 번째 subscriber : ", event.element ?? event)
}.disposed(by: disposeBag)
첫 번째 subscriber : 2
첫 번째 subscriber : 3
두 번째 subscriber : 2
두 번째 subscriber : 3
이걸 마블다이어그램으로 표현하면
구독과 동시에 버퍼에 있던 element가 동작한다.
여기서 새로운 이벤트를 발생시키면
subject.onNext(4)
이때는 구독을 하고 있는 모든 subscriber가 .next 이벤트의 element를 가지고 동작을 수행하게 된다. 콘솔로 찍어 보면
첫 번째 subscriber : 2
첫 번째 subscriber : 3
두 번째 subscriber : 2
두 번째 subscriber : 3
첫 번째 subscriber : 4
두 번째 subscriber : 4
(순서가 섞이지 않고 보장되어서 나오는게 좋은 점인거 같다.)
이때 버퍼는 사이즈가 2이기 때문에 이전에 들어 있던 2값은 버퍼 메모리에서 나가고 3과 4 element가 버퍼에 남게 된다. 이 상태에서 에러 이벤트를 방출하면 첫번째와 두번째는 이미 Subject를 구독하고 있었기 때문에 즉시 .error를 반환하게 된다.
첫 번째 subscriber : 2
첫 번째 subscriber : 3
두 번째 subscriber : 2
두 번째 subscriber : 3
첫 번째 subscriber : 4
두 번째 subscriber : 4
첫 번째 subscriber : error(error)
두 번째 subscriber : error(error)
여기서 새로운 구독자를 추가한다.
subject.subscribe { event in
print("세 번째 subscriber : ", event.element ?? event)
}.disposed(by: disposeBag)
첫 번째 subscriber : 2
첫 번째 subscriber : 3
두 번째 subscriber : 2
두 번째 subscriber : 3
첫 번째 subscriber : 4
두 번째 subscriber : 4
첫 번째 subscriber : error(error)
두 번째 subscriber : error(error)
세 번째 subscriber : 3
세 번째 subscriber : 4
세 번째 subscriber : error(error)
.error 이벤트 방출 -> 새로운 subscribe 순서이지만 버퍼에 있던 element가 동작을 하고 subscriber가 즉시 terminate 되는 것을 볼 수 있다.
BehaviorSubject 또한 비슷한 동작을 하는 것을 알 수 있는데 .completed나 .error 같이 terminate 를 수행하는 이벤트를 방출하더라도 출력되는 값을 보장하고 종료되는 것을 알 수 있다. 만약에 terminate 하고 나서 subscriber가 이벤트를 수행하고 싶지 않은 경우 terminate 이벤트 후 바로 subject를 dispose 시켜 주면 된다. 그렇게 되면 subject가 dispose 되었으므로 세번째 subscriber는 buffer 에 있던 element를 가지고 동작을 수행하지 않는다. 헷갈릴 수 있으니 주의
참고 사이트 및 도서 :
https://dongminyoon.tistory.com/45
RxSwift Reactive programming with swift _raywenderlich.com
'🍎 iOS' 카테고리의 다른 글
[Swift] 코드로 스토리보드 연결하기 how to load UIViewController with Storyboard Programmatically (0) | 2022.01.11 |
---|---|
[RxSwift] Relays에 대해서 알아보기 (0) | 2022.01.10 |
[RxSwift] Traits 와 side effect & Subject 기본 (0) | 2022.01.07 |
[RxSwift] DisposeBag 과 Operator (0) | 2022.01.06 |
[RxSwift] Observable, Operators 그리고 subscribe (0) | 2022.01.05 |
댓글