Mark and Sweep 가비지 컬렉션과 함수 기반 Mediator 패턴 – II

시작하기

지난 포스트에서 함수의 강한 참조를 사용하는 메신저 구현 방법과 이 방법을 사용할 때 변수에 저장되지 않는 개체가 가비지 컬렉션에서 생존하게 되는 현상을 살펴봤습니다. 이러한 현상이 항상 문제라고 볼 수는 없지만 그것을 기대하지 않은 상황에서는 곤란해질 수 있습니다. 이번 포스트에서는 약한 참조를 사용해 메시지 구독이 가비지 컬렉션에 영향을 주지 않도록 하는 방법을 알아봅니다.

I'm too weak. Don't kill me.

  1. Mark and Sweep 가비지 컬렉션과 함수 기반 Mediator 패턴 – I
  2. Mark and Sweep 가비지 컬렉션과 함수 기반 Mediator 패턴 – II

이 포스트에 사용되는 예제 코드는 다중 스레딩에 안전하지 않습니다.

약한 참조

약한 참조Mark and Sweep 가비지 컬렉션에 영향을 주지 않는 개체 참조입니다. 약한 참조를 통해 대상 개체에 접근할 수 있지만 가비지 컬렉션의 그래프 탐색에서 약한 참조는 제외됩니다. 루트 개체로부터 강한 참조만을 통해 탐색할 수 없는 개체는 가비지 컬렉션 대상이되죠. 따라서 약한 참조를 통해 개체에 접근할 때에는 항상 대상 개체의 생존 여부를 검사해야합니다. C#의 경우 .NET Framework 초기 버전부터 제공된 System.WeakReference 클래스와 4.5 버전에 추가된 제네릭 버전의 System.WeakReference<T> 클래스를 사용할 수 있고 Java는 1.2 버전에 추가되어 이후 제네릭으로 변경된 java.lang.ref.WeakReference<T> 클래스를 제공합니다.

대리자에 대한 약한 참조

메시지를 구독하는 개체가 가비지 컬렉션에서 생존하게되는 현상을 피하기 위해 약한 참조를 사용할 수 있습니다. 가장 쉽게 떠올릴 수 있는 것은 대리자에 대한 약한 참조 목록을 관리하는 방법입니다. 대리자에 대한 약한 참조 목록을 사용하도록 메신저 코드를 수정하면 다음과 같습니다.

public class Messenger
{
    public static readonly Messenger Instance = new Messenger();

    private List<WeakReference<Action<string>>> _callbacks;

    public Messenger()
    {
        _callbacks = new List<WeakReference<Action<string>>>();
    }

    public void Subscribe(Action<string> callback)
    {
        _callbacks.Add(new WeakReference<Action<string>>(callback));
    }

    public void Publish(string message)
    {
        var collected = new List<int>();
        for (int i = 0; i < _callbacks.Count; i++)
        {
            var reference = _callbacks[i];
            Action<string> callback;
            if (reference.TryGetTarget(out callback))
            {
                callback.Invoke(message);
            }
            else
            {
                collected.Add(i);
            }
        }
        collected.Reverse();
        foreach (var index in collected)
        {
            _callbacks.RemoveAt(index);
        }
    }
}

[소스코드][실행]

메시지를 발행하는 Publish 메서드는 콜백 함수 대리자의 생존 여부를 검사해 선별적으로 메시지를 보내고 메시지 발행이 완료된 후 대상 개체가 가비지 컬렉션에 의해 수거된 약한 참조들을 목록에서 제거합니다. 예제 코드를 단순하게 하기 위해 다중 스레딩 안정성은 고려되지 않았습니다.

실행 결과입니다.

s1 received "Hello"
s2 received "Hello"

오, 이런! 우리가 원하는 결과가 아니네요. 변수에 저장된 s1 개체까지 가비지 컬렉션 이후 메시지를 수신하지 못합니다. 이유는 s1 개체는 가비지 컬렉션 대상이 아니지만 메신저에 등록된 대리자는 가비지 컬렉션 대상이기 때문입니다. 그래서 단순하게 대리자의 약한 참조 목록을 관리하는 것으로는 우리가 원하는 결과를 얻을 수 없습니다.

인스턴스 메서드의 정적화

이제 조금 더 복잡한 방법을 강구해야 합니다. 우선 약한 참조의 대상이 대리자가 아니라 대리자의 대상 개체여야 합니다. 그래야 구독자 개체가 생존하는 동안 메시지를 정상적으로 수신할 수 있습니다.

또 하나 기억해야 할 것은 지난 포스트에서 설명한 것처럼 대리자가 대상 개체의 참조를 가지고 있다는 것입니다. 그래서 이 참조를 배제할 수 있는 방법을 떠올려야 하는데요, 잘 알려져 있지는 않지만 다행스럽게도 System.Delegate 클래스의 CreateDelegate 메서드는 인스턴스 메서드를 대상 개체의 참조를 가지지 않는 정적 메서드 대리자 형태로 빌드하는 기능을 제공합니다. 이 기능을 사용해 Subscriber 인스턴스가 대상인 Action<string> 형식의 대리자를 대상 개체를 가지지 않는 Action<Subscriber, string> 형식의 대리자로 변형할 수 있습니다. 이 때부터는 메시지 구독자가 대리자 개체에 의해 참조되는 것이아니라 대리자가 실행될 때마다 첫번째 매개변수로 전달되는 것이죠. 그리고 .NET Framework 4.5부터는 System.Reflection.MethodInfo 클래스를 통해서도 이 기능이 노출됩니다.

직접 구현해 보겠습니다. 먼저 구독자에 대한 약한 참조와 정적 대리자를 캡슐화하는 내부 형식을 정의하고 이 형식의 목록을 필드로 선언합니다.

private class Subscription
{
    public WeakReference<Subscriber> Subscriber { get; set; }
    public Action<Subscriber, string> Callback { get; set; }
}

private List<Subscription> _subscriptions = new List<Subscription>();

구독자를 등록하는 Subscribe 메서드는 다음과 같이 수정됩니다.

public void Subscribe(Action<string> callback)
{
    var target = callback.Target;
    var method = callback.Method;
    var delegateType = typeof(Action<Subscriber, string>);
    _subscriptions.Add(new Subscription
    {
        Subscriber = new WeakReference<Subscriber>((Subscriber)target),
        Callback = (Action<Subscriber, string>)method.CreateDelegate(delegateType)
    });
}

Action<string> 형식의 대리자가 Action<Subscriber, string> 형식의 대리자로 빌드되어 저장되는 것을 볼 수 있습니다. 런타임에 대리자를 빌드하는 것은 높은 비용을 필요로 하지만 한 번 빌드된 대리자를 호출하는 것은 일반 메서드를 호출하는 것과 유사한 성능을 보여줍니다.

마지막으로 메시지를 발행하는 Publish 메서드입니다.

public void Publish(string message)
{
    var collected = new List<int>();
    for (int i = 0; i < _subscriptions.Count; i++)
    {
        var subscription = _subscriptions[i];
        Subscriber subscriber;
        if (subscription.Subscriber.TryGetTarget(out subscriber))
        {
            subscription.Callback.Invoke(subscriber, message);
        }
        else
        {
            collected.Add(i);
        }
    }
    collected.Reverse();
    foreach (var index in collected)
    {
        _subscriptions.RemoveAt(index);
    }
}

[소스코드]

실행 결과는 다음과 같습니다.

s1 received "Hello"
s2 received "Hello"
s1 received "World"

드디어 원하는 결과를 얻었네요.

메신저 지원 패키지

직접 메신저를 구현할 수도 있지만 메신저 패턴을 지원하는 패키지를 사용하는 것도 방법입니다. 메신저 패턴 구현체를 포함하는 패키지를 사용할 때 어떤 결과를 얻을 수 있는지 몇 가지 패키지를 대상으로 확인해 봅니다.

MVVM Light Toolkit

MVVM(Model View ViewModel) 패턴 구현체인 MVVM Light Toolkit은 메신저를 제공합니다. MVVM Light Toolkit을 사용한 코드와 실행 결과는 다음과 같습니다.

using System;
using System.Linq;
using GalaSoft.MvvmLight.Messaging;

namespace WeakSubscription
{
    public class Subscriber
    {
        private string _name;

        public Subscriber(string name)
        {
            _name = name;
            Messenger.Default.Register<string>(this, m => Console.WriteLine("{0} received "{1}"", _name, m));
        }

        public string Name { get { return _name; } }
    }

    public class Program
    {
        private static void Main(string[] args)
        {
            var s1 = new Subscriber("s1");
            new Subscriber("s2");

            Messenger.Default.Send("Hello");

            GC.Collect();
            GC.WaitForFullGCComplete();

            Messenger.Default.Send("World");
        }
    }
}

[소스코드]

실행 결과입니다.

s1 received "Hello"
s2 received "Hello"
s1 received "World"

우리가 직접 구현한 메신저를 사용한 코드와 동일한 결과를 보여줍니다.

Xamarin Forms

Xamarin Forms는 iOS, Android, Windows Phone을 지원하는 크로스 플랫폼 네이티브 응용프로그램 개발 도구입니다. Xamarin Forms는 MessagingCenter 클래스를 통해 메신저 패턴을 구현합니다. MessagingCenter를 사용한 코드와 실행 결과는 다음과 같습니다.

using System;
using System.Linq;
using Xamarin.Forms;

namespace WeakSubscription
{
    public class Subscriber
    {
        private string _name;

        public Subscriber(string name)
        {
            _name = name;
            MessagingCenter.Subscribe<object, string>(
                subscriber: this,
                   message: "Greeting",
                  callback: (s, m) => Console.WriteLine("{0} received "{1}"", _name, m));
        }

        public string Name { get { return _name; } }
    }

    public class Program
    {
        private static void Main(string[] args)
        {
            object sender = new object();

            var s1 = new Subscriber("s1");
            new Subscriber("s2");

            MessagingCenter.Send(sender, "Greeting", "Hello");

            GC.Collect();
            GC.WaitForFullGCComplete();

            MessagingCenter.Send(sender, "Greeting", "World");
        }
    }
}

[소스코드]

실행 결과입니다.

s1 received "Hello"
s2 received "Hello"
s1 received "World"
s2 received "World"

이번 포스트에서 구현한 프로그램의 결과와 다릅니다. 오히려 이전 포스트에서 약한 참조를 사용하지 않은 코드와 유사한 결과입니다. 그러면 Xamarin Forms의 MessagingCenter는 강한 참조를 사용해 구현된 것일까요? 그렇지는 않습니다. MessagingCenter의 코드는 분명 구독자에 대한 약한 참조를 사용합니다. 하지만 위와 같은 실행 결과를 얻게되는 이유는 우리가 구현한 것처럼 대리자를 정적 빌드하지 않기 때문입니다. 결국 대리자를 통해 구독자에 대한 강한 참조를 메신저가 유지하게 되죠. 이것이 버그인지 의도한 것인지는 저도 모르겠지만 분명한 것은 약한 참조를 바탕으로한 메시지 구독을 보장할 수는 없다는 것입니다.

마무리

약한 참조를 사용한 메신저 패턴 구현에 대해 고민해봤고 기대했던 결과를 얻었습니다. 하지만 안타깝게도 아직 우리가 구현한 메신저는 여전히 문제점을 가지고 있습니다. 이번 시리즈의 마지막인 다음 포스트에서는 이 문제점에 대해서 살펴보고 Rx(Reactive Extensions)를 사용한 접근법을 설명합니다.

Advertisements

Mark and Sweep 가비지 컬렉션과 함수 기반 Mediator 패턴 – II”에 대한 2개의 생각

  1. 핑백: Mark and Sweep 가비지 컬렉션과 함수 기반 Mediator 패턴 – I | Just hack'em

답글 남기기

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

WordPress.com 로고

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

Twitter 사진

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

Facebook 사진

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

Google+ photo

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

%s에 연결하는 중