[UniRx 입문 강좌 3] IObserver 메세지 종류와 스트림의 수명 관리
본문 바로가기
프로그래밍/Unity

[UniRx 입문 강좌 3] IObserver 메세지 종류와 스트림의 수명 관리

by [아마군] 2019. 9. 30.
반응형

[UniRx 입문 강좌 1] 개념 및 기본 사용법 소개
[UniRx 입문 강좌 2] UniRx 의 핵심, Subject 와 Observable 사용 방법

[UniRx 입문 강좌 4] Operator 활용(1) - Where & Select & SelectMany 사용법

[UniRx 입문 강좌 5] Operator 활용(2) - 다양한 오퍼레이터 소개

[UniRx 입문 강좌 6] 코루틴(Coroutine) 과 UniRx 연동


1. IObserver & IObservable 인터페이스

지난 강의에서 Subject 에 대해 설명하면서 IObserver 인터페이스를 구현하는 과정에서 아래 참고 이미지를 사용했었다.

이번 강좌에서는 IObserver 의 인터페이스인 OnNext 와 OnError, OnComplete 에 대해 좀더 자세히 짚고 가겠다.

 

그만큼 UniRx 를 사용하면서 자주 보게 될 메세지 들이기 때문이다.

 

실제 IObserver 와 IObservable 인터페이스 클래스 소스 코드를 보면 위 도표와 똑같이 각각 3개, 1개의 메소드만 정의 되어 있다.

 

using System;
namespace UniRx
{
public interface IObserver<T>
{
void OnNext(T value);
void OnError(Exception error);
void OnCompleted();
}
}
view raw IObserver.cs hosted with ❤ by GitHub

 

UniRx 에서 사용되는 이벤트 메세지는 이 3가지를 이용해 처리 된다.

 

  • OnNext : 일반적인 이벤트 발생 시 통지
  • OnError : 스트림 처리 중 예외(Exception)발생 시 통지
  • OnComplete : 스트림 종료 통지

Subscribe 메서드는 이 메세지들 중 원하는 것만 받아서 처리할 수 있게각 상황별로 오버로드 되어 있다.

 

Subscribe 오버로드 기능
Subscribe(IObserber observer) 기본형
Subscribe() 모든 메시지를 무시한다
Subscribe(Action onNext) OnNext 만 처리
Subscribe(Action onNext, Action onError) OnNext & OnError
Subscribe(Action onNext, Action onCompleted) OnNext & OnCompleted
Subscribe(Action onNext, Action onError, Action onCompleted) OnNext & OnError & OnCompleted

 

자, 그럼 이 메세지 들이 어떻게 동작하는지 살펴 보자.


2. OnNext

UniRx 에서 이벤트를 통지할 때 대부분 이 OnNext 메세지를 사용하게 될 것이다.

 

기본 사용에 있어서는 이 OnNext 만 사용해도 큰 문제가 없다.

 

var intTest = new Subject<int>();
intTest.Subscribe(x => Debug.Log(x));
intTest.OnNext(2);
intTest.OnNext(50);
intTest.OnNext(100);
//---결과---//
2
50
100
view raw IntTest.cs hosted with ❤ by GitHub

예제와 같이 OnNext 가 호출 될 때마다 정수형 이벤트를 Subscibe 에 통지해 준다.

 


3. OnError

OnError 메세지는 위에서 설명했듯이 스트림 처리 중 예외(Exception) 이 발생하면 통지된다.

 

에러 처리가 필요 없다면 OnNext 와 달리 생략도 가능하다.

 

또한 OnError 메세지가 Subscribe 에 도달한다면 이 스트림은 그 시점에서 바로 구독이 종료 된다는 점을 명심하자.

 

간단한 샘플 코드를 살펴보자.

 

var stringSubject = new Subject<string>();
void Start()
{
// 문자열을 스트림 중간에서 정수로 변환한다
stringSubject
. Select(str => int.Parse( str )) //int 형이 아닌 경우 int.Parse 메소드에서 예외가 발생
. Subscribe(
x => Debug.Log("성공 : " + x), //OnNext 시 처리
ex => Debug.Log("예외 : " + ex) //OnError 시 처리. 생략하면 예외 처리 안함
); //Subscribe 의 오버로드 중 Subscribe(OnNext, OnError)
stringSubject. OnNext("1");
stringSubject . OnNext("2");
stringSubject . OnNext("Hello"); //문자열을 넣으므로 Select 구문의 int.Parse 에서 예외 발생
stringSubject . OnNext("4");
}
//---결과---//
성공 : 1
성공 : 2
예외 : System.FormatException : Input string was not in the correct format
view raw OnError1.cs hosted with ❤ by GitHub

 

스트림을 처리하는 중에 예외가 발생하자 Subscribe에 OnError 메세지가 통지 되고 스트림의 구독이 종료 되어 이후에 발생한 OnNext("4") 는 처리 되지 않았다.

 

만약 예외가 발생한 경우 그에 대한 처리를 해주고 다시 구독을 이어나가고 싶다면 어떻게 해야 할까.

 

var stringSubject = new Subject<string>();
void Start()
{
// 문자열을 스트림 중간에서 정수로 변환한다
stringSubject
.Select(str => int.Parse(str)) //int 형이 아닌 경우 int.Parse 메소드에서 예외가 발생
.OnErrorRetry((FormatException ex) => //예외 정보의 필터링 가능
{
Debug.Log("에외 발생하여 다시 실행");
})
.Subscribe(
x => Debug.Log("성공 : " + x), //OnNext 시 처리
ex => Debug.Log("예외 : " + ex) //OnError 시 처리. 생략하면 예외 처리 안함
); //Subscribe 의 오버로드 중 Subscribe(OnNext, OnError)
stringSubject.OnNext("1");
stringSubject.OnNext("2");
stringSubject.OnNext("Hello");
stringSubject.OnNext("4");
stringSubject.OnNext("5");
}
//---결과---//
성공 : 1
성공 : 2
예외 발생하여 다시 실행
성공 : 4
성공 : 5
view raw OnError2.cs hosted with ❤ by GitHub

이전 예제와 달리 예외가 발생해도 마지막 까지 Subscribe 에 통지를 진행한다.

 

다른 점은 예외가 발생한 Select 와 Subscribe 사이에 OnErrorRetry 가 추가된 점인데 이 OnErrorRetry 는 예외를 중간에서 Catch 한 후 스트림을 재구축 (Subject 에 IObserver 를 다시 등록) 하여 구독을 계속 이어가게 해준다.

 

이로 인해 Subscribe 의 OnError 가 발생하지 않게 되는 것이다.

 

이 예외 처리와 관련한 몇개의 오퍼레이터가 있다.

 

Operators 기능
Retry OnError 발생 시 다시 Subscribe 한다
Catch OnError 발생 시 예외 처리 후 다른 스트림으로 대체한다
CatchIgnore OnError 발생 시 예외 처리 후 OnError 를 무시하고 OnCompleted 로 대체한다
OnErrorRetry OnError 발생 시 예외 처리 후 Subscribe 한다 (시간 지정 가능)

4. OnCompleted

OnCompleted 는 스트림이 완료 되었으므로 이후로는 메세지를 발행하지 않는다 라는 사실을 통지하는 메세지이다.

 

OnError 와 마찬가지로 Subscribe 에 OnCompleted 가 통지되면 해당 스트림은 바로 구독이 종료 된다.

 

이를 이용해 스트림을 파기하려는 시점에 OnCompleted 를 발행하여 구독을 종료 시킬 수 있다.

 

이렇게 구독이 종료된 Subject 는 재사용이 불가능 하며 Subscribe 를 호출해도 바로 onCompleted 가 발생하게 된다.

 

var subject = new Subject<int();
subject.Subscribe(
x => Debug.Log(x),
() => Debug.Log("OnCompleted")
); //Subscribe 의 오버로드 중 Subscribe(OnNext, OnCompleted)
subject.OnNext(1);
subject.OnNext(2);
subject.OnCompleted();
subject.OnNext(4);
subject.OnNext(5);
//---결과---//
1
2
OnCompleted
view raw OnCompleted.cs hosted with ❤ by GitHub

OnCompleted 를 발생 시키면 스트림의 구독이 완전히 중단되는 것을 확인 할 수 있다.

 


5. Dispose

위의 IObserver 의 OnNext, OnError, OnCompleted 와 마찬가지로 Subject 가 구현하고 있는 IObservable 의 "IDispose" 에 대해 알아보자.

 

public interface IObservable<T>
{
IDisposable Subscribe(IObserver<T> observer);
}
view raw IObservable.cs hosted with ❤ by GitHub

Subscribe 의 반환값인 IDisposable 은 가비지컬렉터가 관리하지 않는 리소스를 해제해 주기 위한 C#의 기본 인터페이스이다. (IDisposable.Dispose() 로 해제)

 

Subscribe 가 완료 될 때 이 IDisposable 을 반환 한다는 것은 곧 이 IDisposable 의 Dispose 를 실행하면 스트림의 구독이 종료 된다는 뜻이다.

 

간단한 사용예를 보자.

 

var subject = new Subject<int>();
//IDispose를 저장
var disposable = subject.Subscribe(x => Debug.Log(x), () => Debug.Log("OnCompleted")); //Subscribe(OnNext, OnCompleted) 오버로드
subject.OnNext(1);
subject.OnNext(2);
//Subscribe 종료
disposable.Dispose();
//-----이 아래는 Subscribe 않음-----//
subject.OnNext(3);
subject.OnCompleted();
//---결과---//
1
2
view raw Dispose.cs hosted with ❤ by GitHub

이와 같이 Dispose 를 호출하여 이 스트림의 구독을 중단 할 수 있다.

 

단, OnCompleted 이벤트가 아니라 Dispose() 로 구독을 종료했을 경우 OnCompleted 를 전달하지 않는다는 점을 주의해야 한다.

 

또 한가지, OnCompleted 는 호출하는 Subject 의 모든 스트림의 Subscribe 가 중단 되지만 Dispose 는 Subject 의 스트림 중 원하는 한개의 스트림만 구독을 중단 할 수 있다.

 

아래 샘플을 참고하자.

 

var subject = new Subject<int>();
//IDispose를 저장
var disposable1 = subject.Subscribe(x => Debug.Log("스트림1:" + x), () => Debug.Log("OnCompleted"));
var disposable2 = subject.Subscribe(x => Debug.Log("스트림2:" + x), () => Debug.Log("OnCompleted"));
subject.OnNext(1);
subject.OnNext(2);
//스트림1 만 Subscribe 종료
disposable1.Dispose();
subject.OnNext(3);
subject.OnCompleted();
//---결과---//
스트림1:1
스트림2:1
스트림1:2
스트림2:2
스트림2:3 //스트림1:3 은 Dispose 로 인해 더이상 Subscribe 되지 않는다.
2.OnCompleted
view raw Dispose1.cs hosted with ❤ by GitHub

6. 스트림의 라이프 사이클(Life Cycle)

스트림은 편리하게 사용할 수 있지만 객체의 잦은 생성과 삭제가 퍼포먼스에 큰 영향을 미치는 유니티 특성 상 특히 수명 관리에 신경을 써야 한다.

 

기본적으로 스트림은 Subject 에 포함 되는 일부 또는 그 자체 라고 볼 수 있으며 Subject 가 Destory 되면 스트림도 파기 된다.

 

하나하나의 스트림은 Subscribe 가 끝단에 위치하여 흘러온 데이터를 처리하게 되며 이 각각의 스트림 들은 Subject 에 등록 되어 있다.

 

즉, Subject 가 파기되지 않고 남아있다면 각각의 스트림들도 살아있다는 뜻이 된다.

 

때문에 스트림이 참조하고 있는 오브젝트가 스트림보다 먼저 Destory 되어 버리면 해당 스트림의 처리는 정상적으로 이루어질 수 없음에도 여전히 살아남아 NullReferenceException 을 일으키거나 메모리릭 등 성능 저하와 치명적인 오류를 발생하는 원인이 될 수 있다.

 

하지만 사용될 오브젝트가 런타임에서 결정되거나 파기 타이밍을 매번 코딩하는 단계에서 확인하기 힘든 경우가 많기 때문에 스트림에서 일일이 오브젝트를 검사하는 것도 쉽지 않은 일이다.

 

이런 경우 가장 손쉽게 처리할 수 있는 방법은 AddTo 오퍼레이터를 사용하는 것이다.

 

var subject = new Subject<int>()
subject
.Where(x => x > 5)
.Subscribe(x =>
{
//---처리할 내용들...
//------------------
}).AddTo(gameObject); //지정한 GameObject 가 파기되면 이 스트림을 Dispose 한다.
view raw AddTo.cs hosted with ❤ by GitHub

AddTo 오퍼레이터에 GameObject 의 인스턴스를 지정하면 해당 인스턴스가 Detroy 되는지를 감시하여 자동으로 Dispose 를 호출하여 구독을 중단 시켜 준다.

 


[UniRx 입문 강좌 4] Operator 활용(1) - Where & Select & SelectMany 사용법

 

 

[UniRx 입문 강좌 4] Operator 활용(1) - Where & Select&SelectMany 사용법

[UniRx 입문 강좌 1] 개념 및 기본 사용법 소개 [UniRx 입문 강좌 2] UniRx 의 핵심, Subject 와 Observable 사용 방법 [UniRx 입문 강좌 3] IObserver 메세지 종류와 스트림의 수명 관리 1. 오퍼레이터(Operator..

skuld2000.tistory.com

 

반응형

댓글