https://reactivex.io/documentation/subject.html
ReactiveX - Subject
If you have a Subject and you want to pass it along to some other agent without exposing its Subscriber interface, you can mask it by calling its asObservable method, which will return the Subject as a pure Observable. See Also
reactivex.io
RxSwif에서 Subject는 무엇인가?
이번시간엔 Subject가 무엇인지 알아보고 사용 예시 등을 알아보자.
우선 Subject가 무엇인지 항상 공식문서에서 먼저 한번 찾아보자.
Subject
subject는 Observer나 Observable처럼 행동하는 ReactiveX의 일부 구현체에서 사용 가능한 일종의 교각 혹은 프록시라고 볼 수 있는데, 그 이유는 Subject는 옵저버이기 때문에 하나 이상의 Observable을 구독할 수 있으며 동시에 Observable이기도 하기 때문에 항목들을 하나 하나 거치면서 재배출하고 관찰하며 새로운 항목들을 배출할 수도 있다.
Observable
.... 역시 공식문서를 꼼꼼히 챙겨봐야하지만 개념을 정확히 기술적으로 설명해야 하기에 이해하기 어렵다. 쉽게 한번 풀어보자.
Observable은 이벤트를 생성, 방출하는 것이 목적이고 Observer는 방출된 이벤트를 구독하여 관찰하는 역할은 한다. 그런데 개념 설명에서 Observer나 Observable처럼 행동한다고 한다. 맞다. subject는 둘 다 역할을 한다고 보면 된다. 예시를 한번 살펴보자.
let disposeBag = DisposeBag()
let textArray = Observable.from(["A", "B", "C", "D"])
textArray
.subscribe(with: self) { owner, value in
print("next - \(value)")
} onError: { owner, error in
print("error - \(error)")
} onCompleted: { owner in
print("onCompleted")
} onDisposed: { owner in // onDisposed는 observer가 받는 명시적인 이벤트의 종류로 보지는 않는다.
print("onDisposed")
}
.disposed(by: disposeBag)
/*
next - A
next - B
next - C
next - D
onCompleted
onDisposed
*/
출력을 살펴보면 Observable은 데이터를 방출하고 onCompleted 이벤트가 실행되고 dispose로 리소스까지 정리되어 버린다.
코드를 보면 dispose() 메서드를 직접 호출하여 리소스를 정리한 것도 아니고 강제로 dispose시킨 코드가 아니다. 하지만 dispose되는 것을 확인할 수 있는데 이것은 Observable의 특성이라고 볼 수 있다.
즉 Observable은 이벤트를 생성 및 전달하는 역할만 하는 것이다. 이후 Observer에 의한 이벤트 전달을 다시 받을 수 없기 때문에, 다시 말해 받을 일이 없기 때문에 알아서 데이터를 방출하고 리소스가 정리되는 것이다.
그렇다면 이것을 해결하기 위해 Subject를 사용해서 다음 예시를 살펴보자.
Subject
우선 Subject에는 대표적으로 4가지가 있다고 소개하고 있지만 우선 Observable과의 차이를 살펴보기 위해 subject의 종류 중 하나인 자주 사용되는 BehaviorSubject를 통해 예시를 살표보자.
let textArray = BehaviorSubject(value: ["A", "B", "C", "D"])
textArray
.subscribe(with: self) { owner, value in
print("next - \(value)")
} onError: { owner, error in
print("error - \(error)")
} onCompleted: { owner in
print("onCompleted")
} onDisposed: { owner in // onDisposed는 observer가 받는 명시적인 이벤트의 종류로 보지는 않는다.
print("onDisposed")
}
.disposed(by: disposeBag)
/*
next - ["A", "B", "C", "D"]
*/
이번엔 Subject를 통해 이벤트를 방출했다. 출력을 살펴보면 next 이벤트로 데이터만 방출되고 자동으로 dispose되지 않았다. 예시를 살펴보면 Observable과 차이는 명확하다. Subject는 Observable로써 데이터를 생성, 방출 할 뿐만아니라 Observer로써 값을 관찰하고 이벤트를 보낼수도 있는 것이다.
그럼 onNext 사용해 값을 Subject에게 전달해보자.
let textArray = BehaviorSubject(value: ["A", "B", "C", "D"])
textArray
.subscribe(with: self) { owner, value in
print("next - \(value)")
} onError: { owner, error in
print("error - \(error)")
} onCompleted: { owner in
print("onCompleted")
} onDisposed: { owner in // onDisposed는 observer가 받는 명시적인 이벤트의 종류로 보지는 않는다.
print("onDisposed")
}
.disposed(by: disposeBag)
textArray.onNext(["A1", "B1", "C1"])
textArray.onNext(["D2"])
textArray.onNext(["D3", "E3", "H3"])
/*
next - ["A", "B", "C", "D"]
next - ["A1", "B1", "C1"]
next - ["D2"]
next - ["D3", "E3", "H3"]
*/
최초 데이터가 방출된 이후 onNext로 전달한 데이터까지 잘 방출된 것을 볼 수 있다. Subject의 dispose 시점은 해당 클래스의 인스턴스가 메모리에서 해제되어 disposeBag 인스턴스 또한 소멸되어 dispose가 되거나 개발자가 원하는 시점에 dispose를 하는 방법으로 Subject의 리소스를 제거할 수 있다.
또한 dispose를 통해 리소스를 정리할 수도 있지만 completed, error 이벤트가 전달되면 그 즉시 리소스가 정리된다.
추가적으로 말하자면 ARC 주제에서 봤었던 클로저의 강한참조로 인한 메모리 누수 문제로 인해 subscribe 클로저 또한 약한 참조로 직접 RC를 관리해줄 수 도 있지만 이후에 Rx에 제공하는 메서드를 사용해 참조 문제를 관리할 수 있으므로 코드에는 해결하지 않고 넘어가겠다.
다시 본론으로 돌아와 Subject의 특성을 알아봤으니 그럼 Subject의 종류에 대해 알아보고 각각의 특성도 같이 알아보자.
Subject의 종류
Subject는 공식문서에 나와있듯 4가지 종류가 있으며 각 항목은 다음과 같다.
- BehaviorSubject
- PublishSubject
- AsyncSubject
- ReplaySubject
여기서 자주 쓰이는 Subject는 BehaviorSubject, PublishSubject 2개가 가장 자주 쓰인다. 한번 각 Subject가 어떤 특성을 가지고 있는지 살펴보자.
BehaviorSubject
let subject = BehaviorSubject(value: 200)
subject.onNext(20)
subject.onNext(30) // 방출되지는 않지만 초기값을 대체함
subject
.subscribe(with: self) { owner, value in
print("BehaviorSubject next - \(value)")
} onError: { owner, error in
print("BehaviorSubject error - \(error)")
} onCompleted: { owner in
print("BehaviorSubject onCompleted")
} onDisposed: { owner in
print("BehaviorSubject onDisposed")
}
.disposed(by: disposeBag)
subject.onNext(3)
subject.onNext(4)
subject.on(.next(10))
subject.onCompleted()
subject.onNext(15)
/*
BehaviorSubject next - 30
BehaviorSubject next - 3
BehaviorSubject next - 4
BehaviorSubject next - 10
BehaviorSubject onCompleted
BehaviorSubject onDisposed
*/
위의 Marvel Diagram은 코드를 보고 작동 원리를 생각하면서 보면 더 쉽게 이해갈 것이다. 처음부터 이미지를 보면 잘 이해가 되지 않았다. 뭐 이것은 개인차가 있으므로 어떤 방식으로 학습해도 상관없을 것 같다.
다시 코드를 설명하자면 BehaviorSubject 는 초기값을 가지는 특성을 가진다. 초기값 200을 가지고 있었지만 구독하기 전 onNext 이벤트로 값을 전달받았기 때문에 초기값이 최신화되다가 구독전 초기값은 30으로 설정되었고 이후 구독이 일어나면서 초기값 30부터 이후 onNext로 전달받은 데이터들이 방출된다. 중간에 테스트하기 위해 onComplete 를 호출하였고 원칙에 따라 complete되면서 dispose까지 되는 것을 볼 수 있다. 위에서 complete나 error가 실행되면 리소스가 정리된다고 설명했으니 넘어가겠다.
PublishSubject
let subject = PublishSubject<Int>()
subject.onNext(20)
subject.onNext(30)
subject
.subscribe(with: self) { owner, value in
print("publishSubject next - \(value)")
} onError: { owner, error in
print("publishSubject error - \(error)")
} onCompleted: { owner in
print("publishSubject onCompleted")
} onDisposed: { owner in
print("publishSubject onDisposed")
}
.disposed(by: disposeBag)
subject.onNext(3)
subject.onNext(4)
subject.on(.next(10))
subject.onCompleted()
subject.onNext(15)
/*
publishSubject next - 3
publishSubject next - 4
publishSubject next - 10
publishSubject onCompleted
publishSubject onDisposed
*/
PublishSubject의 특성을 살펴보자면 BehaviorSubject의 특성과는 다르게 초기값을 가지지 않는다.
예전에 MVVM에서 Custom Observable을 만들어서 bind 하는 패턴으로 구현하였었는데 bind 메서드에서 클로저를 한번 실행시키고 안시키고 차이가 궁금했었는데 멘토님께서 Rx를 배우면 초기값을 가지고 안가지는 Observable이 존재한다고 했었는데 이 Subject 2개를 보고 말씀해준신게 아닌가 생각이 든다.
출력을 보면서 동작원리를 파악해보자면 PublishSubject는 초기값이 없는 특성을 가지기 때문에 구독전에 next로 전달된 데이터는 아무 의미가 없다 구독 이후에 전달된 값만 의미가 있다고 보면된다. 구독 이후의 전달된 값이 순서대로 방출되고 있고 complete가 실행되면서 Stream이 끊어지며 리소스가 정리된다. 그래서 마지막 전달값인 '15'는 방출되지 않는 것을 볼 수 있다.
AsyncSubject
let subject = AsyncSubject<Int>()
subject.onNext(20)
subject.onNext(30)
subject.onNext(40)
subject.onNext(50)
subject.onNext(60)
subject
.subscribe(with: self) { owner, value in
print("AsyncSubject next - \(value)")
} onError: { owner, error in
print("AsyncSubject error - \(error)")
} onCompleted: { owner in
print("AsyncSubject onCompleted")
} onDisposed: { owner in
print("AsyncSubject onDisposed")
}
.disposed(by: disposeBag)
subject.onNext(3)
subject.onNext(4)
subject.on(.next(10))
subject.onCompleted()
subject.onNext(15)
/*
AsyncSubject next - 10
AsyncSubject onCompleted
AsyncSubject onDisposed
*/
AsyncSubject는 소스 Observable로부터 배출된 마지막 값(만)을 배출하고 소스 Observalbe의 동작이 완료된 후에야 동작한다.
AsyncSubject의 특성은 맨 마지막 값을 뒤 이어 오는 옵저버에 전달한다. 해당 observer의 의 Stream이 끊어지는 순간(complete, error)에 마지막에 전달받았던 값을 next로 방출하고 dipose되는 특성이 있다.
ReplaySubject
let subject = ReplaySubject<Int>.create(bufferSize: 3)
subject.onNext(20)
subject.onNext(30)
subject.onNext(40)
subject.onNext(50)
subject.onNext(60)
subject
.subscribe(with: self) { owner, value in
print("ReplaySubject next - \(value)")
} onError: { owner, error in
print("ReplaySubject error - \(error)")
} onCompleted: { owner in
print("ReplaySubject onCompleted")
} onDisposed: { owner in
print("ReplaySubject onDisposed")
}
.disposed(by: disposeBag)
subject.onNext(3)
subject.onNext(4)
subject.on(.next(10))
subject.onCompleted()
subject.onNext(15)
/*
ReplaySubject next - 40
ReplaySubject next - 50
ReplaySubject next - 60
ReplaySubject next - 3
ReplaySubject next - 4
ReplaySubject next - 10
ReplaySubject onCompleted
ReplaySubject onDisposed
*/
ReplaySubject의 특성은 옵저버가 구독을 시작한 시점과 관계 없이 소스 Observable(들)이 배출한 모든 항목들을 모든 옵저버에게 배출한다. bufferSize로 개수에 따라 구독 전에 next로 전달한 데이터를 bufferSize만큼 방출하고 구독 뒤에는 원래대로 방출하고 complete, error가 전달되면 dispose되고 리소스가 정리된다.
지금까지 Subject의 특성을 알아봤고 그 종류까지 자세하게 알아봤다. 그럼 여기까지 내용이 틀리다면 댓글 달아주세요.
'iOS > RxSwift' 카테고리의 다른 글
RxSwift - API Request, Error Handling (+ Button Tap Stream), Single Traits (0) | 2023.11.16 |
---|---|
RxSwift - Unicast, Multicast (1) | 2023.11.06 |
RxSwift - Dispose, Disposable, DisposeBag (2) | 2023.11.03 |
RxSwift 알아보기 (1) | 2023.11.01 |