Reactive MVVM(Model-View-ViewModel) 모바일 응용프로그램 아키텍쳐

Envicase(앱스토어) 개발팀은 클라이언트 응용프로그램에 사용하기 위해 Reactive MVVM 아키텍쳐를 설계했습니다. 이 포스트는 Reactive MVVM 아키텍쳐를 만든 이유와 구조를 설명합니다. 슬라이드와 본 포스트를 함께 읽어보시기를 권장합니다.

reactive-mvvm-architecture

Reactive MVVM 아키텍쳐

시작하기

MVVM(Model View ViewModel) 패턴은 UI를 가지는 응용프로그램의 디자인과 논리를 분리시켜 코드를 구조화하고 테스트가능도(testability)를 높여줍니다. 소셜 네트워크 모바일 응용프로그램 개발에 MVVM 패턴을 적용하면서 다양한 경로로 노출되는 데이터에 대한 무결성과 일관성을 유지하기 위한 방법을 고안해야 했습니다. 처음에는 매개자 패턴(Mediator Pattern)을 고려했지만 불변 개체(immutable objects)와 Rx(Reactive Extensions)를 MVVM 패턴에 결합하는 방법이 구조적으로 복잡하지 않으면서도 효율적으로 이 문제를 해결한다고 판단했습니다. 이 문서는 MVVM 디자인 패턴을 기반으로 불변 개체와 Rx를 활용해 상태를 관리하는 응용프로그램 아키텍쳐와 이 기법을 효과적으로 사용하는 데에 필요한 이슈들을 설명하고 이런 구조를 Reactive MVVM 아키텍쳐라고 부릅니다.

MVVM 디자인 패턴

MVVM(Model View ViewModel) 디자인 패턴은 2005년 Microsoft의 John Gossman이 소개한 WPF(Windows Presentation Foundation) 응용프로그램을 개발하기 위해 만들어진 아키텍쳐 설계 패턴입니다. MVVM 패턴에서 뷰모델(ViewModel)은 뷰에 데이터를 제공하고 프리젠테이션 논리를 담당합니다. 뷰는 양방향 바인딩(2-way Binding)과 바인딩 전파(Binding Propagation)를 사용해 이들과 뷰 요소를 연결합니다. 뷰모델은 양방향 바인딩의 도움으로 뷰에 대한 정보 없이 작성되기 때문에 뷰 인스턴스 없이도 동작하고 테스트 가능합니다. 최근에는 WPF와 Silverlight 외에도 Xamarin Forms(iOS/ Android/ Windows Phone), Angular.js, Ember.js, Knockout.js, RoboBinding(Android) 등의 도구를 통해 다양한 플랫폼에서 사용됩니다.

mvvm

MVVM 응용프로그램

service-client-app-with-mvvm

MVVM 서비스 클라이언트 응용프로그램

상태 동기화

소셜 네트워크 모바일 응용프로그램에서 하나의 컨텐트(모델)는 다수의 뷰에 표현될 수 있습니다. 예를 들어 envicase에서 하나의 쇼케이스는 다음과 같은 경로로 노출될 수 있습니다.

  • 뉴스 피드 탭
  • 탐색 탭에서 쇼케이스를 탭해서 쇼케이스 상세 뷰로 이동
  • 알림 목록 탭에서 사용자를 탭해서 사용자 프로필 상세 뷰로 이동

multiple-views-per-single-model

이런 조건에서 사용자가 뉴스 피드 탭에서 해당 쇼케이스를 엔비하면 쇼케이스는 엔비된 상태가 되고 엔비 수가 증가하게 되는데 이 때 탐색 탭과 알림 목록 탭을 통해 노출되는 뷰도 동일한 상태로 갱신되어야합니다. 즉 동일한 컨텐트를 노출하는 모든 뷰의 상태가 동기화되어야합니다.

동기화 흐름

MVVM 패턴에서 뷰모델은 INotifyPropertyChanged 인터페이스의 PropertyChanged 이벤트를 사용해 속성 값의 변경을 외부에 알립니다. 뷰 역시 PropertyChanged 이벤트를 주시하며 뷰모델의 속성과 연결된 UI 요소를 갱신합니다. 그렇기 때문에 뷰모델의 속성이 변경되면 바인딩되어있는 UI 요소가 갱신됩니다. 마찬가지로 동일한 컨텐트를 표현하는 뷰를 갱신하려면 해당 뷰에 바인딩된 뷰모델의 속성을 변경해주면 됩니다.

컨텐트 상태를 변경시킨 뷰모델이 동일한 컨텐트를 표현하는 뷰모델에게 직접 변경된 정보를 전달할 수도 있습니다.

sync-between-viewmodels

하지만 이런 방식은 뷰모델이 응용프로그램 구조에 대해 잘 알고 있어야하기 때문에 뷰와 뷰모델의 구성이 복잡해지고 응용프로그램 규모가 커질수록 동기화 흐름의 복잡도가 급격하게 증가합니다. 예를 들어 envicase 뉴스 피드 탭의 쇼케이스 뷰모델에 의해 쇼케이스 상태가 변경된 경우 쇼케이스 뷰모델은 탐색 탭과 알림 목록 탭의 구조에 대해 알고 있어야 같은 쇼케이스를 처리하는 뷰모델을 검색해 상태를 동기화할 수 있습니다.

sync-graph

단방향 흐름

상태를 관리하는 저장소를 만들고 데이터 흐름을 단방향으로 제한하면 상태 동기화의 복잡도 증가를 억제할 수 있습니다. 컨텐트를 표현하는 뷰모델은 저장소가 발행하는 메시지를 구독하여 컨텐트가 변경되었음을 인지합니다. 뷰모델이 컨텐트 상태를 변경하는 작업을 수행하면 직접 상태를 동기화하는 대신 변경된 내용을 저장소에 전달하기만 합니다.

아직까지는 매개자 패턴(Mediator Pattern)과 유사해 보입니다.

oneway

이렇게 하면 컨텐트 상태를 구독하는 코드는 변경된 상태를 반영하는 것에만, 명령 코드는 상태를 변경하는 작업에만 집중할 수 있게됩니다. 뷰모델은 응용프로그램 구조에 대해 신경쓸 필요가 없으며 응용프로그램 규모가 증가해도 상태 동기화 작업의 복잡도는 증가하지 않습니다.

complexity

불변(immutable) 모델 개체

MVVM 패턴에서 일반적으로 사용되는 뷰모델 구조를 쇼케이스로 예를 들면 모델과 뷰모델의 코드는 다음과 같습니다.

public sealed class ShowcaseModel
{
    public long Id { get; }
    public bool IsEnvied { get; set; }
    public int EnvyCount { get; set; }
}

public class ShowcaseViewModel : ViewModel
{
    private ShowcaseModel _showcase;

    public long Id { get { return _showcase.Id; } }

    public bool IsEnvied
    {
        get { return _showcase.IsEnvied; }
        set
        {
            if (value == _showcase.IsEnvied)
                return;
            _showcase.IsEnvied = value;
            NotifyPropertyChanged("IsEnvied"); // Raises 'PropertyChanged' event
        }
    }

    public int EnvyCount
    {
        get { return _showcase.EnvyCount; }
        set
        {
            if (value == _showcase.EnvyCount)
                return;
            _showcase.EnvyCount = value;
            NotifyPropertyChanged("EnvyCount"); // Raises 'PropertyChanged' event
        }
    }
}

viewmodel-properties

그리고 ShowcaseViewModel 개체를 바인딩하는 뷰(XAML) 코드는 다음과 유사하게 됩니다.

<Button BackgroundColor="{Binding IsEnvied, Converter={...}}" />
<Label Text="{Binding EnvyCount, StringFormat='...'}" />

뷰모델은 모델의 각 속성을 자신의 속성으로 감싸서 상태를 노출하고 PropertyChanged 이벤트를 발생시켜 속성이 변경되었음을 외부에 알립니다. 이런 구조에서 사용자가 쇼케이스를 엔비할 때 다음과 같은 문제가 발생합니다.

  • 컨텐트 상태에 대한 무결성이 무너질 수 있습니다. 쇼케이스 모델을 예로 들면 IsEnvied 속성이 True이면서 EnvyCount0인 상태의 컨텐트가 뷰를 통해 사용자에게 노출될 수 있습니다.

  • 한 번의 컨텐트 수정에 대해 뷰의 레아아웃 계산과 그리기 작업 여러 번 발생하게 됩니다. 이 작업들은 비용이 아주 높기 때문에 가능한 줄이는 것이 좋습니다.

IsEnvied EnvyCount PropertyChanged Validity
False 0 Valid
True 0 "IsEnvied" Invalid
True 1 "EnvyCount" Valid

뷰모델이 모델의 각 속성을 자신의 속성으로 감싸지 않고 모델 개체를 직접 노출하는 속성을 제공하는 방법이 이 문제들의 해결책이 될 수 있습니다.

public class ShowcaseViewModel : ViewModel
{
    private ShowcaseModel _model;

    public ShowcaseModel Model
    {
        get { return _model; }
        set { SetProperty(ref _model, value); }
    }
}

그러면 뷰의 바인딩 코드는 다음과 유사하게 됩니다.

<Button BackgroundColor="{Binding Model.IsEnvied, Converter={...}}" />
<Label Text="{Binding Model.EnvyCount, StringFormat='...'}" />

뷰모델이 컨텐트를 모델의 각 속성 수준이 아니라 모델 개체 수준으로 관리하는 것입니다. 컨텐트 상태가 변경되면 모델 개체의 속성이 변경되는 것이 아니라 변경된 상태의 새로운 모델 인스턴스로 교체됩니다. 이렇게 하면 컨텐트가 여러 가지 상태값을 가지더라도 한 번의 컨텐트 상태 변경에 대해 PropertyChanged 이벤트는 한 번만 발생하고 컨텐트 상태의 무결성이 유지됩니다.

Model PropertyChanged Validity
{ IsEnvied: False, EnvyCount: 0 } Valid
{ IsEnvied: True, EnvyCount: 1 } "Model" Valid

단, 조건이 있는데 모델 개체가 불변성(immutability)을 가져야한다는 것입니다. 불변 개체는 초기화된 후 상태가 수정될 수 없습니다. 모델 개체가 뷰모델 속성을 통해 그대로 노출되면 외부 코드에 의해 상태가 수정될 가능성이 생기고 많은 문제들을 야기합니다. 불변성은 이런 위험을 원천적으로 차단합니다.

model-properties

불변 모델 반응형 스트림

지금까지 언급된 상태를 동기화하는 방법을 정리하면 이렇습니다.

  • 컨텐트 상태는 불변성을 가지는 모델 개체로 캡슐화
  • 뷰모델은 모델 개체를 속성으로 뷰에 노출
  • 컨텐트 상태가 변경되면 새로운 모델 인스턴스 발행

좀 더 단순하게 한 마디로 요약하면 ‘불변성을 가진 모델 개체의 반응형 스트림’으로 표현할 수 있습니다.

stream

Reactive MVVM 아키텍쳐에서 뷰모델은 모델 스트림을 구독합니다. 모델 상태를 변경하야하는 상황이 되면 뷰모델은 새로운 상태를 나타내는 모델 개체를 만들어 스트림에 푸시하고 이 새로운 모델 개체는 스트림을 구독하고 있는 모든 뷰모델에 발행됩니다.

sync-via-stream

반응형 스트림은 IObservable 인터페이스로 정의됩니다.

public interface IObservable<out T>
{
    IDisposable Subscribe(IObserver<T> observer);
}

public interface IObserver<in T>
{
    void OnCompleted();
    void OnError(Exception error);
    void OnNext(T value);
}

반응형 스트림을 구현하는데에 Rx(Reactive Extensions)는 적격입니다. Rx는 반응형 개체 모델에 대한 구현체로 반응형 개체 + LINQ + 스케줄링으로 표현할 수 있습니다. 즉, Rx를 사용해 데이터 스트림을 만들거나(반응형 개체) 데이터 스트림을 가공하고(LINQ) 지정한 논리 시간에서(스케줄링) 구독할 수 있습니다. C#, JavaScript, Java, Ruby, Python 등의 언어를 지원합니다.

.NET 프레임워크에서 IObservable 인터페이스는 Rx의 일부가 아니며 코어 라이브러리에 포함되어 있습니다.

모델

Reactive MVVM 아키텍쳐에서 모델이 스트리밍 대상이 되려면 식별자를 제공해야합니다. 그리고 식별자는 다른 식별자와의 같음 여부를 확인할 수 있어야합니다. 같음 여부 확인은 IEquatable 인터페이스를 사용할 수 있습니다. 이런 기능을 제공하는 추상 기반 클래스를 정의합니다.

public abstract class Model<TModel, TId>
    where TModel : Model<TModel, TId>
    where TId : IEquatable<TId>
{
    private readonly TId _id;

    protected Model(TId id)
    {
        _id = id;
    }

    public TId Id { get { return _id; } }
}

스트리밍 대상 모델 클래스는 Model 클래스를 상속받습니다. long 형식은 IEquatable 인터페이스를 구현하기 때문에 식별자 형식으로 사용될 수 있습니다. 앞서 정의한 ShowcaseModel 클래스가 Model 클래스를 상속받도록 수정합니다.

public sealed class ShowcaseModel : Model<ShowcaseModel, long>
{
    private readonly bool _isEnvied;
    private readonly int _envyCount;

    public ShowcaseModel(long id, bool isEnvied, int envyCount)
        : base(id)
    {
        _isEnvied = isEnvied;
        _envyCount = envyCount;
    }

    public bool IsEnvied { get { return _isEnvied; } }
    public int EnvyCount { get { return _envyCount; } }
}

스트림 저장소

스트림 저장소는 지정한 모델 식별자에 대한 스트림을 제공하고 새 개정(revision)의 모델 형상을 스트림에 푸시하는 기능을 노출합니다.

public static class StreamStore<TModel, TId>
    where TModel : Model<TModel, TId>
    where TId: IEquatable<TId>
{
    public static IObservable<TModel> GetStream(TId id);
    public static void Push(TModel model);
}

모델-뷰모델(Model-ViewModel)

모델-뷰모델은 하나의 모델을 관리하는 뷰모델입니다. 모델-뷰모델은 스트림 저장소에 모델의 식별자를 사용해 해당 컨텐트를 제공하는 스트림을 요청하고 반환된 스트림을 구독합니다. 이후 IObserver.OnNext() 메서드를 통해 새로운 개정의 모델 인스턴스가 뷰모델로 전달되면 Model 속성 값을 수정합니다.

public abstract class ModelViewModel<TModel, TId> : ViewModel
    where TModel : Model<TModel, TId>
    where TId: IEquatable<TId>
{
    private TModel _model = null;
    public TModel Model
    {
        get { return _model;}
        set { SetProperty(ref _model, value); }
    }

    protected virtual void OnNext(TModel next)
    {
        Model = next;
    }
}

스트림 구독

아직 모델-뷰모델이 스트림을 구독하는 코드가 등장하지 않았습니다. Mark and Sweep GC(Garbage Collection)를 사용하는 플랫폼에서 매개자 패턴(Mediator Pattern)이나 관찰자 패턴(Observer Pattern)을 사용할 때 항상 신경써야하는 것들 중 하나가 메모리 누수(memory leak)입니다. 스트림 구독 역시 동일한 맥락에서 생각해 볼 수 있습니다. 푸시된 새 개정의 모델 인스턴스를 구독자에게 전달하려면 스트림은 구독자에게 접근할 수 있어야합니다. 스트림의 구독자 목록이 모델-뷰모델의 참조를 가지면 수명이 다한 모델-뷰모델 인스턴스가 GC 대상에서 제외될 수 있고 이것은 바로 메모리 누수를 의미합니다.

이런 현상을 막기 위해 약한 참조를 사용한 구독을 만들 수 있습니다.

public class WeakSubscription<T> : IDisposable
{
    private readonly WeakReference<IObserver<T>> _reference;
    private readonly IDisposable _subscription;

    public WeakSubscription(IObservable<T> observable, IObserver<T> observer)
    {
        _reference = new WeakReference<IObserver<T>>(observer);
        _subscription = observable.Subscribe(onNext: OnNext);
    }

    private void OnNext(T value)
    {
        IObserver<T> observer;
        if (_reference.TryGetTarget(out observer))
        {
            observer.OnNext(value);
        }
        else
        {
            _subscription.Dispose();
        }
    }

    public void Dispose()
    {
        _subscription.Dispose();
    }
}

WeakSubscription는 약한 참조를 이용해 IObservableIObserver를 연결합니다. 모델-뷰모델이 WeakSubscription 클래스를 사용하여 스트림을 구독하는 코드는 다음과 같습니다.

public abstract class ModelViewModel : ViewModel
{
    private readonly TId _id;
    private readonly IObserver _observer;
    private readonly IDisposable _subscription;

    protected ModelViewModel(TId id)
    {
        _id = id;

        var stream = StreamStore.GetStream(id);
        _observer = Observer.Create(onNext: OnNext);
        _subscription = new WeakSubscription(stream, _observer);
    }

    ~ModelViewModel()
    {
        _subscription.Dispose();
    }
}

weak-subscription

이렇게 하면 모델-뷰모델은 활성화 되어있는 동안 스트림의 데이터를 구독하며 UI에서 제거된 뷰와 연결된 뷰모델은 GC 대상이됩니다. 그리고 뷰모델이 GC에 의해 삭제될 때 스트림 구독도 중단되기 때문에 메모리 누수를 발생시키지 않습니다.

스위치 연산자

서비스로부터 컨텐트의 상태를 업데이트하는 작업은 주 스레드가 멈추지 않도록 비동기적으로 처리해서 응용프로그램의 반응성을 높이는 것이 일반적입니다. 그런데 모델-뷰모델에서 모델을 업데이트하는 작업이 시작되고 이 작업이 종료되기 전에 동일한 모델을 업데이트하는 또 다른 작업이 시작되는 상황도 발생합니다. 이런 경우 한 번만 Model 속성 값이 수정되도록 하면 레이아웃 계산과 그리기 작업이 최소화됩니다. 그리고 컨텐트 변경 작업이 포함된 경우라면 마지막에 시작된 작업의 결과가 뷰에 반영되는 것이 바람직한데 비동기 작업의 시작 순서와 종료 순서가 일치함을 보장할 수 없다는 것이 문제가 됩니다.

Rx의 Switch() 연산자의 도움을 받아 이 문제들을 해결할 수 있습니다. Rx의 Switch() 연산자는 지연된 개체(Futures Pattern)의 스트림에 대해 완료되지 않은 지연된 개체 중 마지막에 입력된 결과만을 노출하는 유용한 도구입니다. 지연된 개체에 대한 스트림을 만들고 이 스트림에 Switch() 연산을 추가하여 결과 값을 모델 스트림에 푸시하는 파이프라인을 모델-뷰모델에 추가합니다.

switch

public abstract class ModelViewModel<TModel, TId> : ViewModel
{
    private readonly Subject<IObservable<TModel>> _spout;

    protected ModelViewModel(TId id)
    {
        _spout = new Subject<IObservable<TModel>>();
        _spout.Switch()
              .Where(next => next != null)
              .Subscribe(next =>
              {
                  if (next.Id.Equals(_id) == false)
                      throw new InvalidOperationException();
                  StreamStore<TModel, TId>.Push(next);
              });
    }

    protected virtual void Push(IObservable<TModel> next)
    {
        _spout.OnNext(next);
    }
}

TaskIObservable보다 지연된 개체를 표현하는 데에 더 많이 사용되며 await 키워드의 직접적인 지원을 받는 강력한 도구입니다. Rx는 TaskIObservable 구현체로 변형해주는 ToObservable() 확장 메서드를 제공합니다. ToObservable() 메서드를 사용해 Task를 직접 푸시할 수 있는 Push() 메서드 오버로드를 지원합니다.

public abstract class ModelViewModel<TModel, TId> : ViewModel
{
    protected void Push(Task<TModel> next)
    {
        Push(next.ToObservable());
    }

    protected void Push(Func<Task<TModel>> next)
    {
        Push(next.Invoke().ToObservable());
    }
}

지연된 개체는 이름과는 다르게 개체가 지연되었거나 지연되지 않았음을 의미합니다. 그렇기 때문에 지연되지 않은 즉각적인 결과도 수용합니다. 지연되지 않은 모델 개체에 대한 지원을 모델-뷰모델 클래스에 추가합니다.

지연되지 않은 지연된 개체를 0 단위 시간만큼 지연되었다고 해석할 수도 있습니다.

public abstract class ModelViewModel<TModel, TId> : ViewModel
{
    protected void Push(TModel next)
    {
        Push(Task.FromResult(next).ToObservable());
    }
}

병합(Coalescing) 연산자

서비스로부터 컨텐트의 최신 개정을 가져올 때 항상 모든 속성이 포함되지 않을 수도 있습니다. 컨텐트 목록을 조회할 때에는 기본 속성만 포함되고 하나의 컨텐트에 대한 상세 정보를 조회할 때에는 모든 속성이 포함되도록 서비스가 구성된 경우가 그렇습니다.

사용자 모델이 식별자와 사용자 이름, 프로필 사진을 기본 정보라고 정의하고 팔로우 대상 수와 팔로워 수를 단일 컨텐트 조회에서만 제공되는 추가 정보라고 하면 사용자 모델 클래스를 다음과 같이 정의할 수 있습니다.

public sealed class UserModel : Model<UserModel, string>
{
    private readonly string _userName;
    private readonly string _profilePhotoUri;
    private readonly int? _followeeCount;
    private readonly int? _followerCount;

    public UserModel(
        string id,
        string userName,
        string profilePhotoUri,
        int? followeeCount,
        int? followerCount)
        : base(id)
    {
        _userName = userName;
        _profilePhotoUri = profilePhotoUri;
        _followeeCount = followeeCount;
        _followerCount = followerCount;
    }

    public string UserName { get { return _userName; } }
    public string ProfilePhotoUri { get { return _profilePhotoUri; } }
    public int? FolloweeCount { get { return _followeeCount; } }
    public int? FollowerCount { get { return _followerCount; } }
}

list-detail

이런 경우 상세 정보 조회를 통해 모든 속성 값을 가지고 있는 모델이 목록 조회를 통해 가져온 모델에 의해 교체되면 응용프로그램은 해당 모델의 일부 정보를 잃어버리게 됩니다.

without-coalescing

이 문제를 해결하기 위해 모델-뷰모델이 새로운 모델 형상을 푸시받을 때 푸시받은 모델 인스턴스를 그대로 적용하지 않고 새 개정이 가지지 않은 속성 값에 대해서 기존 인스턴스의 정보를 활용하는 것을 고려할 수 있습니다.

여러 프로그래밍 언어가 Null 병합 연산자(Null-Coalescing Operator)를 제공합니다. 왼쪽 피연산자가 Null이 아니면 왼쪽 피연산자가 반환되고 왼쪽 피연산자가 Null이면 오른쪽 피연산자가 반환되는 연산자입니다. Null 병합 연산자를 모델 개체의 각 추가 정보에 적용하는 연산자를 만들면 우리가 원하는 기능을 구현할 수 있습니다.

C#은 기존 형식을 수정하지 않고 공용 멤버만 사용하는 새로운 멤버 함수를 추가하는(추가한 것처럼 보이게 하는) 기능인 확장 메서드를 제공합니다. 이 확장 메서드를 사용하여 Coalesce 연산자를 UserModel 클래스에 추가합니다.

C#에서 ?? 연산자(Null-Coalescing Operator)는 연산자 오버로딩(Operator Overloading) 대상이 아닙니다.

public static class ModelExtensions
{
    public static UserModel Coalesce(this UserModel user, UserModel other)
    {
        if (user == null)
            throw new ArgumentNullException("user");

        if (other == null)
            return user;

        if (other.Id != user.Id)
            throw new ArgumentException();

        if (user.Equals(other))
            return user;

        return new UserModel(
            user.Id,
            user.UserName,
            user.ProfilePhotoUri,
            user.FolloweeCount ?? other.FolloweeCount,
            user.FollowerCount ?? other.FollowerCount);
    }
}

coalescing

이제 사용자에 대한 모델-뷰모델이 모델 스트림으로부터 새 모델 개정을 전달 받을 때 Coalesce 연산자를 사용하도록 OnNext() 메서드를 재정의합니다.

public class UserViewModel : ModelViewModel<UserModel, string>
{
    protected override void OnNext(UserModel next)
    {
        var current = Model;
        base.OnNext(next.Coalesce(current));
    }
}

같음 확인

MVVM 패턴을 지원하는 프레임워크는 일반적으로 다음과 유사한 코드를 사용해 뷰모델의 속성을 설정하는 기능을 제공합니다.

public class ViewModel : INotifyPropertyChanged
{
    protected bool SetProperty<T>(
        ref T field,
        T value,
        [CallerMemberName] string propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, value))
            return false;

        field = value;
        NotifyPropertyChanged(propertyName);
        return true;
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected void NotifyPropertyChanged(string propertyName)
    {
        var handler = PropertyChanged;
        if (handler != null)
            handler.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

SetProperty 메서드는 fieldvalue를 같음 여부를 확인하고 두 값이 같으면 불필요한 이벤트 발생을 막기 위해 속성 설정 프로세스를 중단하고 false를 반환합니다. 만약 형식 T가 같음 여부 확인에 대해 별도의 코드를 제공하지 않으면 기본적으로 object.ReferenceEquals를 사용해 동일한 참조인지 여부를 검사합니다. 그렇기 때문에 서비스를 통해 모델의 최신 형상을 가져왔을 때 기존에 존재하던 인스턴스와 포함하고 있는 데이터가 모두 일치하더라도 Model 속성은 변경되고 불필요한 레이아웃 계산과 그리기 작업이 시작됩니다.

non-equatable-model

이런 현상을 피하기 위해 모델 클래스가 IEquatable 인터페이스를 구현할 수 있습니다. EqualityComparer 클래스가 제공하는 논리는 대상 형식이 IEqutatable 인터페이스를 구현할 경우 같음 비교 작업을 IEquatable.Equals(T other) 메서드에 맡깁니다. 그렇기 때문에 모델 클래스가 구현하는 IEquatable.Equals(T other) 메서드가 자신과 동일한 데이터를 가지는 다른 모델 인스턴스에 대해 true를 반환하면 뷰모델은 PropertyChanged 이벤트를 발생시키지 않습니다.

public sealed class ShowcaseModel :
    Model<ShowcaseModel, long>,
    IEquatable<ShowcaseModel>
{
    public bool Equals(ShowcaseModel other)
    {
        if (other == null)
            return false;

        if (object.ReferenceEquals(this, other))
            return true;

        return Id == other.Id &&
               _isEnvied == other._isEnvied &&
               _envyCount == other._envyCount;
    }
}

equatable-model

마무리

모델과 뷰가 일대다 관계를 가지는 MVVM 패턴 기반의 응용프로그램에서 모델의 무결성과 일관성을 유지하기 위해 MVVM 패턴을 확장한 Reactive MVVM 아키텍쳐를 사용할 수 있습니다. Reactive MVVM 아키텍쳐의 특징은 다음과 같습니다.

  • 모델 데이터는 단방향으로 흐릅니다.
  • 모델 개체는 불변성을 가집니다.
  • 반응형 스트림을 통해 모델의 새로운 개정이 발행됩니다.
  • 뷰모델은 메모리 누수를 막기 위해 약한 스트림 구독을 사용합니다.
  • 뷰모델은 모델 변경을 최소화하고 가장 최근의 모델 개정을 유지하기 위해 Switch() 연산이 포함된 지연된 모델의 스트림 파이프라인을 거쳐 모델 스트림에 새로운 모델 개정을 푸시합니다.
  • 데이터 조회 서비스에 따라 제공되는 정보 구성에 차이가 있을 때 데이터 유실을 막기 위해 Coalescing() 연산을 사용할 수 있습니다.
  • 모델 클래스는 IEquatable 인터페이스를 구현해 모델 변경을 최소화합니다.

reactive-mvvm-architecture

다음은…

Envicase 개발팀은 Reactive MVVM 아키텍쳐를 지원하는 프레임워크를 개발하고 배포하려는 계획을 가지고 있습니다. 프레임워크는 아키텍쳐를 구성하는 여려 요소들을 쉽게 구현하도록 도와주는 도구들이 포함될 예정입니다. 각종 기반 클래스, 병합 연산과 같음 비교를 선언적으로 구성하는 특성, 디버깅을 위한 모델 추적 도구 등을 구상하고 있습니다.

Advertisements

Reactive MVVM(Model-View-ViewModel) 모바일 응용프로그램 아키텍쳐”에 대한 6개의 생각

  1. parkheesung

    Rx가 뭔지, 왜 필요한지 한참 찾아보고 있었는데 딱히 와닿는게 없었습니다.
    그런데 이규원님의 이 포스트에서 MVVM과 함께 설명하니, 바로 이해가 됐습니다.
    좋은 내용 감사합니다.

    응답
  2. 노연진

    좋은 글 감사합니다.
    개인적인 생각입니다만 ViewModel과 Model을 클래스로 결합하는 건 확장성에 있어서 많으 제약들을 가져 올 수 있다고 봅니다. Model이 List 형태라던지 아니면 다른 타입의 Model을 2개 이상 갖는 다던지 각각 할일이 다른데 편의성만 고려된게 아닌지 고민 해 볼 필요가 있다고 생각됩니다.

    응답
  3. 임현수

    Rx MVVM 이해에 큰 도움이 되었네요 감사합니다~
    https://github.com/envicase/flip 은 C# 으로 구현이 되어있던데
    혹시 RxJava 로 구현된 예제는 없는지요?
    안드로이드 앱을 Rx MVVM 으로 RxJava을 사용해 구현중인데 마땅한 예제를 구하기가 쉽지 않네요.

    응답
  4. Gyuwon 글의 글쓴이

    댓글 확인이 너무 늦었네요. 아쉽지만 아직은 C# 구현체 밖에 없습니다. 현재 Flip 프로젝트는 더 확장할 목적으로 회사 조직에서 반응형 프로그래밍을 다루는 별도의 조직으로 이사하는 중입니다. 이 작업이 완료되면 Java나 다른 언어를 대상으로 한 개발을 계획하고 있습니다. 관심 감사합니다.

    응답

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

Google+ photo

Google+의 계정을 사용하여 댓글을 남깁니다. 로그아웃 / 변경 )

%s에 연결하는 중