본문 바로가기
Unity/UniRx & UniTask

UniRx의 작동 원리(4) Observable의 본질

by Pretty Garbage 2022. 12. 18.

Observable은 생성 방법과 타이밍에 따라 움직임이 다릅니다.

어떤 때에 어떤 행동을 하는지 확실히 알아둘 필요가 있습니다.

 

Subject -> Operator -> Operator       Observer

 

즉, 옵저버와 연결되지 않은 상태 IObservable을 상속 받고 정의한 것만으로는 메시지 전달이 안됩니다.

Observable 각 요소를 연결하고 실행하려면 IObserver의 Subscribe()를 연결해줘야 합니다.

따라서 이 상태에서는 메시지를 수신 할 수 없으므로 "연결 확인"이라는 처리가 필요합니다. 이 연결을 확인하는 데 필요한 처리는

Subscribe()의 실행입니다.

 

예시

 

var subject = new Subject<string>();

//string을 받으면 현재 + 스페이스 + 입력받은 string 을 연결하고 마지막에 최종 결과를 반환하도록 하는 Observable
//Operator 설명 :
//Scan : 현재 메세지의 값과 이전 메세지 결과를 이용해 누적 처리를 반복한다. LinQ의 Aggregate와 비슷한 역할을 한다.
//Last : Observable의 마지막 메시지만 통과시키고 싶다.
var appendStringObservable =
    subject
        .Scan((previous, current) => previous + " " + current)
        .Last();

//이 타이밍에 Observable이 움직인다.
appendStringObservable.Subscribe(Debug.Log);

subject.OnNext("I");
subject.OnNext("am");
subject.OnNext("a");
subject.OnNext("student");
subject.OnCompleted();
subject.Dispose();

Subscribe()를 한 시점에서 I am a student 라는 문자열을 구독받고 종료가 될 것입니다.

하지만 I 와 am 사이에 구독처리를 한다면 a student 라는 문자열만 구독받게 될 것입니다.

 

!! 잠깐 설명을 하자면 Scan 이라는 Operator는 LinQ의 Aggregate와 비슷한 역할을 하는 친구인데 조금 더 딴길로 새자면

 

private void AggregateExample()
{
    var members = new List<string>();
    members.Add("Ho-sung");
    members.Add("Jong-ho");
    members.Add("Ji-hyuk");
    members.Add("Jin-wook");
    members.Add("Hyun-Jin");
    
    //일반적인 방법
    foreach (var member in members)
        Debug.Log(member);
    
    //LinQ 중간에 처리가 자유자재이다.
    var aggregate = members.Aggregate((x1, x2) => x1 + ", " + x2);
    Debug.Log(aggregate);
}

List<T>.Aggregate()를 사용하면 누적시켜가면서 중간에 처리해줘야할 일들에 대해서 처리가 가능합니다. 물론 foreach라고 불가능한 것은 아니지만 조금더 깔끔하지 않나 생각합니다.

 

Operator 인스턴스 생성

Operator는 독립적으로 사용할 수 없습니다. IObservable과 IObserver를 연결하는 데에 사용되어지며, 동일한 Operator를 여러번

Subscribe 하게 된다면 Operator의 인스턴스를 새로 생성하여 별도의 Observable로서 분리되어지는 행동을 취합니다.

즉, 어떤 말이냐면

 

private void DivideObservable()
{
    var subject = new Subject<int>();
    var observable = subject.Do(x=>Debug.Log(x.ToString()));

    observable.Subscribe(x => Debug.Log("First Subscribe" + x));
    observable.Subscribe(x => Debug.Log("Second Subscribe" + x));
}

이렇게 별개의 인스턴스로서 관리된다라는 이야기입니다.

 

 

Hot과 Cold Observable

 

UniRx 자료들을 검색하다보면 이 hot 과 cold에 관한 자료들이 많이 나오는데 한 번 정리하고자 합니다.

아무도 구독하지 않고 작동하고 있지 않는 Observable을 Cold Observable이라고 합니다.

반대로 작동하고 있는 Observable을 Hot Observable이라고 합니다.

 

Cold Observable을 다루는 것이 어려울 수 있으므로, Hot으로 변환해야하는 경우가 있을 수 있습니다.

Cold -> Hot으로 변환하는 가장 간단한 방법은 Subject<T>를 사용하는 방법입니다.

지난 포스팅에서 Subject<T>는 IObserver<T>와 IObservable<T> 구현해야된다. 이야기 했습니다.

이 성질을 이용해 Hot으로 변환을 실행하고 싶은 Observable에 대해서 Subject<T>를 사용해 Subscribe()를

실행합니다. 따라서 연결의 IObserver<T>는 Subject<T>에 대해 Subscribe()를 실행해야 합니다.

 

Observable(Hot) --- / ---> Operator(Cold) -> Operator(Cold) --/ --> Observer 

 

와 같은 구조에서 

Observable(Hot) ---> Operator(Hot) -> Operator(Hot) ---> Subject(Hot) --> Observer

 

위와 같이 Hot Observable로 변환이 가능합니다.

 

Subject 대신 전용 Operator 사용

Hot 변환을 위해서 매번 Subject를 만들고 꼬리에 연결시키는 작업은 대단히 귀찮습니다. 따라서

UniRx에서는 전용 Operator가 제공됩니다.

수동으로 Subject를 사용하는 것보다 전용 Operator는 사용하기 편하기 때문에 Hot변환할 때에 전용 Operator를

사용을 추천합니다. 

 

private void AppendStringTwo()
{
    var subject = new Subject<string>();
    
    IConnectableObservable<string> appendStringObservable =
        subject
            .Scan((previous, current) => previous + " " + current)
            .Last()
            .Publish(); //Hot으로 변환시켜주는 Operator

    var disposable = appendStringObservable.Connect();

    //이 타이밍에 Observable이 움직인다.
    appendStringObservable.Subscribe(Debug.Log);
    
    subject.OnNext("I");
    subject.OnNext("am");
    subject.OnNext("a");
    subject.OnNext("student");
    subject.OnCompleted();
    
    disposable.Dispose();
    subject.Dispose();
}

Hot으로 변경하기 위한 Operator는 몇가지 더 있지만 일단은 간단하게 예제를 보이고 Hot으로 변경하면 어떠한 장점이 있는지 기술하고자합니다.

 

Hot변환은 Observable의 끝에 Subject<T>를 추가하는 처리 와 같은 말입니다. 따라서 Hot으로 변환된 Observable은 다음과 같은 기능을 갖게됩니다.

 

  • 이미 Operator가 동작을해 메시지를 전달하고 있는 상태가 된다.
  • 실행 중인 Operator를 공유할 수 있습니다. (새 인스턴스를 구독할 때마다 만들 필요가 있음)

앞서 말한 내용 중 Operator는 구독할 때마다 새인스턴스를 생성한다는 특성이 있었습니다. 그러나 Hot Observable을 공유한다면

새로운 인스턴스 생성을 방지하고 기존 Operator를 그대로 사용하면서 여러 곳에서 구독을 가능케 합니다.