왕초보 R린이..RxSwift 개념 2탄..! - Subject

2025. 3. 2. 05:36Apple/RxSwift

 

 

To. 미래독자 혹은 나 자신에게..(연관글 아래 링크 참고해주세요)

RxSwift - Subject 를 읽기 전 RxSwift 가 처음이고 동시성에 대해 가볍게 알고 가고 싶으신가요?

제가 이전에 공부하고 정리한 아래 글들을 먼저 읽어보시는 것도 추천드려요.

(틀린 부분이 있다면 피드백은 언제든지 환영입니다!)


 

RxSwift 개념을 다시 접한게 근 5개월만인 것 같다. Subject 의 개념과 활용법을 살펴보기 앞서, 정의에서 언급하는 것에 Observable 과 Observer 가 있기에 제대로 개념을 잡고 넘어가보자 한다. RxSwift 개념 1탄에서 Observable 에 대한 타입과 큰 틀인 라이프사이클 흐름만 가볍게 짚고 넘어갔기 때문에 개념 정리의 필요성을 느꼈다. 

Observable

  • Observable 이 객체를 배출할 때까지 기다릴 필요 없이 어떤 객체가 배출(emit) 되면 그 시점을 감시하는 관찰자를 Observer 안에 두고 그 관찰자를 통해 배출 알림을 받는다. 이러한 패턴이 동시성 연산을 가능하게 한다.
  • Observable: 단방향으로 데이터를 발행하는 존재
  • Observer: 발행된 데이터를 구독하여 반응하는 존재

공식문서의 내용이지만, 참 개념설명이 어렵고 와닿지 않았다. 배출, 관찰자, 알림, 동시성.. 비유로 굳이 든다면, 요즘 시대에 맞게 유튜브 채널구독자의 개념으로 설명할 수 있을 것 같다.

  • 유튜브 채널은 새로운 영상을 업로드하는 주체다. 이걸 Observable이라고 생각하면 된다.
  • 새로운 영상이 올라오면 알림을 받고 시청하는 구독자들이 있을 텐데, 이들이 Observer이다.
  • 그런데 모든 사람이 알림을 받는 것은 아니다. 유튜브 채널을 구독한 사람들만 새로운 영상이 올라왔다는 알림을 받을 수 있다. 이처럼 구독을 관리하는 것이 Subscription의 역할이다.

 

  • ReactiveX 에서는 옵저버에 의해 임의의 순서에 따라 병렬로 실행되고 결과는 나중에 연산된다.
임의의 순서란 정확히 뭘 의미하는 걸까? Observable 이 데이터를 배출하는 타이밍을 개발자가 직접 제어할 수 없고, 옵저버가 데이터를 받는 순서도 예측할 수 없다는 것이다. 

       

→ 메서드 호출보다는 Observable 안에 데이터를 조회하고 변환하는 메커니즘을 정의한 후,

  1. Observable이 이벤트를 발생시키면
  2. 옵저버의 관찰자가 그 순간을 감지하고 준비된 연산을 실행시켜
  3. 결과를 리턴하는 매커니즘 때문에, observable 을 구독한다고 표현

 ReactiveX 는 비동기 실행과 병렬처리를 기반으로 하기에 임의의 순서로 실행, 배출(혹은 방출)되는 것이다.

Subject

RxSwift 에서 Observable 과 Observer 의 역할을 동시에 수행하는 특별한 존재

  • 외부에서 직접 데이터를 발행(onNext, onError, onCompleted) 할 수 있으면서
  • 동시에 여러 구독자에게 데이터스트림을 전달할 수 있는 멀티캐스트(multicast) observable 이다.

Subject 는 브릿지 역할을 하며, 기존의 비동기 데이터 흐름을 외부 입력과 연결하거나, 여러 구독자에게 동일한 데이터를 동시에 전달하는데 유용하다.

 

2. Subject의 역할

2.1 브리지 역할

외부 이벤트 연결: 일반 Observable데이터의 흐름을 스스로 정의하지만, Subject외부에서 발생한 이벤트를 받아 직접 onNext() 등을 호출해 이벤트를 흘려보낼 수 있다.

데이터 중계: 여러 Observer가 동일한 데이터 스트림을 받아야 할 때, 하나의 Subject가 데이터 소스로부터 받은 이벤트를 다수의 구독자에게 중계한다.

2.2 핫(Hot) Observable

핫 스트림: Subject는 구독 시점과 상관없이 이미 발생한 이벤트는(Subject 종류에 따라 다르지만 PublishSubject의 경우) 전달되지 않거나, 특정 타입(Behavior, Replay)의 경우 최근 이벤트를 전달한다. 반면 일반 Observable은 콜드(Cold) Observable로, 구독할 때마다 새롭게 데이터가 생성된다.

3. 주요 Subject 종류와 특성

PublishSubject

정의: 구독 시점 이후에 발생하는 이벤트만 구독자에게 전달

역할: 단순히 외부 이벤트를 받아 구독자에게 실시간으로 전달하고자 할 때 사용

상호작용: 구독 전에 onNext()로 발생한 이벤트는 무시되므로, 실시간 이벤트 스트림이 중요한 경우 유용함.

BehaviorSubject

정의: 초기값을 가지고 있으며, 구독 시점에 마지막(혹은 초기) 값을 즉시 전달한 후 이후 이벤트도 전달한다.

역할: 상태(state)를 표현할 때 유용하다. 예를 들어, UI의 현재 상태나 데이터의 최신 값을 항상 보존해두어 새 구독자에게 제공하는 상황에 적합

상호작용: 다른 연산자들과 결합할 때 최신 상태 값에 기반한 연산을 수행할 수 있음.

ReplaySubject

정의: 버퍼 크기를 지정하여 구독 시, 버퍼에 저장된 최근 N개의 이벤트를 모두 재생(replay)해준다.

역할: 과거의 일정 범위 내의 이벤트 기록을 보존하고, 새 구독자에게 동일한 데이터를 제공해야 할 때 사용한다.

상호작용: 여러 구독자가 서로 다른 시점에 구독하더라도, 일정 기간 동안 발생한 이벤트를 동일하게 받을 수 있으므로, 데이터 동기화 및 캐싱에 유리하다.

추가로, AsyncSubject라는 Subject도 있는데, 이는 Observable의 작업이 완료되었을 때 마지막 값을 전달하는 특징이 있다.

 

4. Subject와 다른 요소들과의 상호작용

Observable/Observer와의 관계

Subject는 Observer로서 다른 Observable에 구독(subscribe)되어, 그 데이터 흐름을 받아서 자신이 가진 스트림으로 전달할 수 있다.

Subject는 Observable로서 외부에서 직접 onNext() 등을 호출해 이벤트를 발생시켜, 다수의 Observer(구독자)에게 데이터를 전달한다. 예를 들어, 네트워크 요청을 Observable로 처리하는 대신, Subject를 사용하여 외부에서 수동으로 데이터를 주입한 후 이를 여러 UI 컴포넌트에 전달할 수 있다. 

연산자와의 결합

데이터 변형: Subject가 방출하는 이벤트에 대해 다양한 연산자(map, filter, flatMap 등)를 체이닝하여 데이터 변형, 에러 처리, 스케줄링 등을 적용할 수 있다.

멀티캐스팅: Subject를 사용하면 하나의 데이터 소스를 여러 곳에 동시에 전달할 수 있기 때문에, 네트워크 요청의 결과나 사용자 입력 이벤트를 여러 컴포넌트에서 동시에 처리할 때 유리하다.

메모리 관리

Subject 역시 Observable의 구독 관리 원칙을 따른다. DisposeBag을 사용하여 구독이 해제될 때, Subject에 연결된 리소스를 정리하는 것이 중요하다.

 


그럼 Subject 종류들을 활용한 예제 코드를 만들어볼까? 

내가 원했던 요구사항 

PublishSubject 의 역할: 구독 시점 이후에 발생하는 이벤트, Observable 이 배출한 항목들을 옵저버에게 배출 

BehaviorSubject의 역할:  옵저버는 소스 Observable이 가장 최근에 발행한 항목(또는 아직 아무 값도 발행되지 않았다면 맨 처음 값이나 기본 값)의 발행을 시작하며 그 이후 소스 Observable(들)에 의해 발행된 항목들을 계속 발행

ReplaySubject의 역할: 버퍼에 저장된 최근 N개의 이벤트를 모두 다시 재생(replay)하는 설정할 수 있는 기능과 환경을 만들어보고 싶었다. 즉, 정해진 버퍼크기만큼 이벤트를 저장해 새 구독자가 생길 때 그 버퍼 내에 저장된 이벤트들을 순서대로 다시 재생해주는 역할을 한다. 

 

PublishSubject 이벤트 케이스

 

위의 화면처럼 구독 버튼을 누르고 Send chat 버튼을 눌러야 데이터가 전송되고, 그냥 텍스트를 입력하고 Send 하면 구독되지 않아 데이터가 전달되지 않는다. 다시 Subscribe chat 을 누르면 기존에 있던 챗들이 지워지고, 새로 실시간  데이터로 업데이트 된다.

 


  
private func setupPublishSubjectActions() {
// (1) 메시지 전송
chattingView.sendButton.rx.tap
.subscribe(onNext: { [weak self] in
guard let self = self else { return }
let message = self.chattingView.textField.text ?? ""
guard !message.isEmpty else { return }
// 메시지 발행
self.publishSubject.onNext(message)
self.chattingView.textField.text = ""
})
.disposed(by: disposeBag)
// (2) 새로 구독 (Subscribe Chat)
chattingView.subscribeButton.rx.tap
.subscribe(onNext: { [weak self] in
guard let self = self else { return }
// 기존 구독 dispose (중복 구독 방지)
self.currentChatSubscription?.dispose()
// TextView 초기화
self.chattingView.textView.text = ""
// 새 구독
let newSubscription = self.publishSubject
.subscribe(onNext: { [weak self] chat in
self?.appendText(self?.chattingView.textView, "Chat received: \(chat)")
})
self.currentChatSubscription = newSubscription
self.appendText(self.chattingView.textView, "[New PublishSubject subscriber added]")
})
.disposed(by: disposeBag)
}

  
chattingView.sendButton.rx.tap
.subscribe(onNext: { [weak self] in
...
self.publishSubject.onNext(message)
})
.disposed(by: disposeBag)

 

sendButton이 탭 될 때 클로저가 호출된다. 메시지가 비어있지 않다면 onNext 부분에서 호출해 메시지를 방출한다.

publishSubject 는 이 시점에 구독중인 Subscriber 에게 이벤트를 전달한다. TextField 를 비워 다음 입력을 준비한다. 

-> 구독자가 없다면(Subcribe Chat을 누르지 않을때), onNext 로 이벤트를 발행해도 아무도 이 이벤트를 받지 못한다. 

 

BehaviorSubject 이벤트 케이스

 

BehaviorSubject 이벤트를 확인할 수 있게, 마지막으로 배출된 값을 즉시 전달해준다. 구독후 직전 메시지를 한번 보고 싶다면 Subscribe Nick 버튼을 눌러주고 입력하고, 다음 닉네임을 수정해 입력해도 이후 값까지 발행해준다.

 

ReactiveX - Subject 공식문서: BehaviorSubject 타입의 데이터스트림

 

  • BehaviorSubject는 가장 최근에 발행한 값을 기억하고 있다가 새로운 옵저버가 구독하면 그 값을 먼저 발행한다.
  • 첫 번째 옵저버는 구독 시점에서 최근 값(빨간색)을 받음.
  • 두 번째 옵저버는 더 늦게 구독했기 때문에, 그 시점에서 가장 최근 값(핑크색)을 받음.
  • X 표시는 스트림이 종료된 상황을 의미한다 
    - onNext: 새로운 값이 배출(혹은 방출)할 때 호출되는 이벤트다. 즉, 새로운 값이 들어올 때 실행된다. 
    - onCompleted(정상적으로 종료된 경우): 사용자가 입력을 마치고 더이상 데이터를 입력하지 않는 경우
    - 에러가 발생해 스트림 종료: 네트워크 오류, 데이터처리 오류
  • 이후에 발생하는 데이터는 모든 구독자에게 동일하게 전달됨.

이처럼 BehaviorSubject는 여러 개의 데이터를 발행할 수 있다.


  
private func setupBehaviorSubjectActions() {
// (1) 닉네임 설정
demoView.behaviorSetButton.rx.tap
.subscribe(onNext: { [weak self] in
guard let self = self else { return }
let nickname = self.demoView.behaviorTextField.text ?? ""
guard !nickname.isEmpty else { return }
// BehaviorSubject에 새 닉네임
self.behaviorSubject.onNext(nickname)
self.demoView.behaviorTextField.text = ""
})
.disposed(by: disposeBag)
// (2) 구독 (Subscribe Nick)
demoView.behaviorSubscribeButton.rx.tap
.subscribe(onNext: { [weak self] in
guard let self = self else { return }
// 기존 구독 dispose
self.currentBehaviorSubscription?.dispose()
// "현재 닉네임" Label 초기화
self.demoView.behaviorCurrentLabel.text = "[New BehaviorSubject subscriber added]\n"
// 새 구독: "가장 최근 닉네임" + 이후 닉네임 전달
let newSubscription = self.behaviorSubject
.subscribe(onNext: { [weak self] nick in
self?.demoView.behaviorCurrentLabel.text =
"[New BehaviorSubject subscriber added]\nCurrent Nickname: \(nick)"
})
self.currentBehaviorSubscription = newSubscription
})
.disposed(by: disposeBag)
}

 

BehaviorSubject 랑 PublishSubject 구독하고 최신 값을 업데이트하는게 너무 비슷한데?🤔

 

ReplaySubject는 특정 버퍼 크기로 전달한다. 그래서 그런지 PublishSubject 와 BehaviorSubject 와 구분이 간다. 근데 이 두개는 은근 비슷한 것 같다. 언제 BehaviorSubject 와 PublishSubject 를 쓸 지 잘 파악해야 할 것이다.

 

BehaviorSubject"현재 상태"를 저장하고 구독자가 가장 최신 값을 받을 필요가 있을 때
PublishSubject"이벤트 기반"으로 구독한 이후의 값만 받을 때 (구독 전 발행 값은 절대 받을 수 없다.)

 

즉, 구독 전 발생한 값을 받을 수 있냐 없냐의 차이일 것 같다. 닉네임변경은 현재 상태 유지를 위함이고, 채팅 메시지(혹은 상태메시지라고 가정한다면) 이벤트 기반 데이터스트림이기 때문에 PublishSubject 타입을 차용해야할 것이다.  


 

ReplaySubject 이벤트 케이스

ReactiveX - Subject 공식문서: ReplaySubject 타입의 데이터스트림

 

(그림마다 약간씩 다를 수 있으나, 보통 빨강(R), 초록(G), 파랑(B) 이벤트가 있고, 구독 시점 이후의 흐름을 보여준다.)

  1. Subject가 이벤트를 방출
    • 시간 축(→)을 따라 빨간 공(R), 초록 공(G), 파란 공(B)이 순서대로 방출
    • ReplaySubject(bufferSize: 2)라면, 최근 2개의 이벤트를 내부 버퍼에 저장
    • 빨강(R) → 초록(G) → 파랑(B)이 왔을 때, 내부 버퍼에는 “초록(G)”, “파랑(B)” 두 개가 저장되어 있는 상태가 된다.
      • 빨강(R)은 가장 먼저 온 이벤트이므로, 새로 들어온 파랑(B)에 밀려 버퍼에서 제거
  2. 어떤 시점에서 새로운 Subscriber가 구독
    • 그림에서 보면, 세 번째 이벤트(파랑)까지 이미 발생한 뒤에 구독이 이루어짐.
    • ReplaySubject는 “최근 2개의 이벤트”를 새로 구독한 Subscriber에게 바로 내보낸다(Replay).
      • 즉, 구독과 동시에 초록(G), 파랑(B)을 즉시 전달받는다.
  3. 이후 새롭게 들어오는 이벤트도 계속 전달
    • 구독한 시점 이후에 새 이벤트(예: 주황, 보라 등)가 들어오면, 그 이벤트들도 Subscriber가 실시간으로 받게 됨.
    • 물론, ReplaySubject 내부 버퍼도 계속 업데이트된다.

⚠️ 유의: ReplaySubject 을 활용할 때

 

기본적으로 Observable은  이벤트가 순차적이고 예측 가능한 방식이다.

아니아니 위에서는 비동기적이고 임의의 순서이면서 예측할 수 없다며?!??!?! 라고 반감이 들 수 있다. 

기본 전제는 정상적인 상황 즉, Subject 는 동기적, 단일 스레드에서 Observable 계약에 따른 이벤트가 순차적으로 예측 가능하게 전달된다.

그럼 반대되는 상황인 멀티스레드, 비동기상황에서는 onNext 같은 메서드를 여러 스레드에서 동시 호출한다면? 순차적 전달이 깨질 수 있음으로 이런 경우에 ReplaySubject 는 Observable 계약이 위반될 수 있다는 것이다. 

따라서 ReplaySubject를 옵저버로 사용할 때에도, 만약 여러 스레드에서 동시 호출이 발생한다면 Observable이 본래 보장하는 순차적 전달이 보장되지 않아 순서가 모호해지는 문제가 발생할 수 있다.

 "순차적 전달"은 안전하게 호출되었을 때의 계약이고, "임의의 순서"는 동시 호출에 따른 부작용을 경고하는 부분이다.

 

요약하면, ReplaySubject는 “구독 시점 전”에 발생한 이벤트도, 설정된 버퍼 크기만큼 새 구독자에게 전달해준다는 특징이 있다.


  
private func setupReplaySubjectActions() {
// (1) 알림 전송
demoView.replaySendButton.rx.tap
.subscribe(onNext: { [weak self] in
guard let self = self else { return }
let dateString = DateFormatter.localizedString(
from: Date(),
dateStyle: .short,
timeStyle: .medium
)
let notification = "Notification at \(dateString)"
// ReplaySubject에 알림 발행
self.replaySubject.onNext(notification)
})
.disposed(by: disposeBag)
// (2) 구독 (Subscribe Notifications)
demoView.replaySubscribeButton.rx.tap
.subscribe(onNext: { [weak self] in
guard let self = self else { return }
// 기존 구독 dispose
self.currentReplaySubscription?.dispose()
// TextView 초기화
self.demoView.replayTextView.text = "[New ReplaySubject subscriber added]"
// 새 구독: 최근 2개 알림 + 이후 알림 수신
let newSubscription = self.replaySubject
.subscribe(onNext: { [weak self] note in
self?.appendText(self?.demoView.replayTextView, "Received: \(note)")
})
self.currentReplaySubscription = newSubscription
})
.disposed(by: disposeBag)
}

 

 

RxSwift의 전반적 개념과 이용방법, 흐름에 대해서 1편에서는 다룰려고 했고, 이번에는 Observable, observer의 역할과 그 두 역할을 하는 Subject 에 대해 공부해봤다. Subject 의 3가지 종류가 구독하는 시기, 방출하고 데이터를 업데이트 하는 방식들이 다르다는 걸 코드로 접하면서 깨닫게 된 것 같다. 또한 그런 타입들이 쓰이는 상황에 대해 좀 더 고민해보는 시간이었던 것 같다. 

다른 Observable이나 연산자와 결합해 데이터 흐름을 제어 UI 바인딩이나 네트워크 데이터 처리 등 다양한 상황에서 활용 가능하다고 한다. 이와 같이 Subject는 RxSwift의 중요한 구성 요소로, 기본 개념을 확실히 이해하면 복잡한 비동기 데이터 흐름을 효과적으로 관리할 수 있다. 또한, 이런 특성들은 필요한 상황에서 유용하게 활용되고, 사용자 인터랙션, 실시간 데이터 업데이트, 캐싱된 데이터 제공 등에 쓰인다는 걸 기능 구현을 보며 간접적으로 체감한 것 같다. 이젠 코드들을 내 예전 개인 프로젝트에도 RxSwift 방식으로 짜 볼 차례가 다가온 것 같다. 

 

다음엔 정말 RxCocoa 에 대해 살펴봐야할 것 같다. ㅎㅎ

 

 

참고자료
ReactiveX - Subject
chatGPT 와의 대화

 

 

 

잘못된 정보나 자료에 대한 피드백과 댓글은 환영입니다! 

 

RxSwift 공부시간: 5-6시간

글쓰기 소요시간: 5시간

 

 

'Apple > RxSwift' 카테고리의 다른 글

[RxSwift] 왕초보 R린이..RxSwift 개념 1탄..!  (0) 2024.10.27