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

시작하기

요즘 자주 접할 수 있는 프로그래밍 언어에 대한 키워드 중 하나가 함수형 프로그래밍입니다. 꼭 함수형 언어가 아니라도 많은 언어들이 함수형 프로그래밍 특징들을 스펙에 포함하고 있습니다. 올해 초에는 Java 스펙에도 람다가 추가되었고 C# 2.0에 클로저(closure)가 추가된 것이 벌써 10년전, 저 역시 계속 함수형 프로그래밍을 공부하고 있습니다. 그런데 언어별 함수형 프로그래밍 특징들의 구현 방법에 대해 잘 모르면 낭패를 보는 경우가 가끔 있습니다. 지금부터 다룰 시리즈에서는 JVM이나 CLR 같은 Mark and Sweep 가비지 컬렉션을 사용하는 플랫폼에서 주의해야 할 내용 몇 가지를 Mediator 패턴을 사용해 소개합니다. 언어는 C#을 기준으로 설명하지만 개념적인 내용은 람다를 사용하는 Java 프로그래머들도 꼭 한 번 고민해 볼 내용입니다.

총 3개의 포스트로 진행됩니다. 이번 포스트에서는 강한 참조로 구현되는 메신저에서 발생할 수 있는 문제를 다루고 두번째 포스트는 약한 참조를 사용해 이 문제를 해결하는 방법과 실제로 약한 참조를 사용하는 메신저들을 분석합니다. 마지막 세번째 포스트에서는 약한 참조를 사용한 구현이 가진 문제점을 살펴보고 다시 강한 참조로 돌아가 Rx(Reactive Extensions)를 사용하는 방법을 알아봅니다.

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

기술 블로그 포스트라는 것이 다 그렇지만, 최대한 정확한 정보를 전달하려 노력하더라도 제 지식 범위 내에서 작성되기 때문에 틀린 내용이 포함될 수 있습니다. 이 포스트에서 굳이 이 점을 언급하는 이유는 다룰 문제들이 골치아픈 것들이라 제가 모르는 더 좋은 방법이 있다면 고수님들의 조언이 절실하기 때문입니다.

Mark and Sweep 가비지 컬렉션

Mark and Sweep은 JVM과 CLR 등에서 사용되는 가비지 컬렉션 방법으로 루트 개체들로부터의 그래프 탐색을 통해 개체의 생존 여부를 결정합니다. 즉 루트 개체로부터 참조를 통해 접근될 수 있는 개체들은 살아남고 그렇지 않은 개체들은 제거되는데, 예외적으로 약한 참조에 의해서만 접근될 수 있는 경우는 제거 대상이 됩니다.

대리자(Delegate)

대리자는 JVM에는 없는 특징입니다. 그렇기 때문에 Java 8의 람다는 인터페이스를 기반으로 구현되었죠. 하지만 인스턴스 메서드가 호출되는 과정의 중요한 점이 설명되기 때문에 CLR 언어를 모르는 Java 프로그래머분들도 넘어가지 마시고 꼭 한 번 읽어보시기를 권장합니다. 개인적으로 대리자는 지금의 발전된 C#을 있게한 일등공신이라고 생각합니다. 언어 공장 Anders Hejlsberg의 혜안이라고 봐야할까요? 그리고 가끔 대리자가 C# 2.0에 추가되었다는 자료를 접할 수 있는데 이것은 잘못된 사실이며 클로저를 위한 delegate 키워드가 C# 2.0에 추가되어 생긴 오해인 것 같습니다. 대리자는 C# 초기 버전부터 빠질 수 없는 언어 특성이었습니다.

대리자는 CLR 기반 언어에서 사용되는 함수(메서드)를 캡슐화하는 일급함수 도구입니다. C++ 영향을 많이 받은 C#은 함수 포인터의 단점 몇 가지를 보강해 대리자를 만들었는데 그 중 하나가 매개변수 목록과 반환 형식이 같으면 인스턴스 메서드와 정적 메서드를 동일한 시그니처로 간주한다는 점입니다. 엄밀히 말하자면 인스턴스 메서드와 정적 메서드는 호출시 전달되는 정보가 차이가 납니다. 바로 this 참조입니다. 인스턴스 메서드는 대상 개체가 가진 데이터에 접근할 수 있어야하기 때문에 this 참조가 매개변수와 함께 전달되어야하죠. C++는 이 문제를 언어 수준에서 해결해 주지 않습니다. 그래서 C++ 프로그래머들은 this 포인터와 함수 포인터를 캡슐화해 사용하는 방법을 사용하기도 하는데, 대리자가 바로 이런 방법을 사용합니다. 대리자는 메서드에 대한 정보 이외에 대상 개체의 참조를 담고 있고 이를 Target 속성을 통해 노출합니다. 그렇기 때문에 C#(과 다른 CLR 언어)에서 프로그래머는 별다른 처리 없이 인스턴스 메서드를 손쉽게 일급함수로 사용할 수 있습니다.

TargetProperty

대리자 구조에 대한 개념적 그림. 주의해야 할 것은 붉은 색으로 표시된 참조.

정석 메서드에 대해 대리자의 Target 속성은 null 참조를 반환합니다.

하지만 이렇게 편리하다고 해서 대리자가 대상 개체의 참조를 가진다는 사실을 아예 잊어버리면 가끔씩 메모리 누수라는 재앙의 씨앗이 되기도 합니다. 몇 년 전 어떤 분에게 자신이 개발에 참여한 소프트웨어가 시간이 갈 수록 메모리 사용량이 증가하는 이유는 ‘CLR 자체에 메모리 누수가 존재하기 때문’이라는 설명을 들은 적이 있습니다. 실상 코드를 뒤져보니 진짜 원인은 한시적 생명주기를 갖는 개체들이 응용프로그램 인스턴스가 발행하는 이벤트를 구독하고 있었기 때문이었죠. 아직 무슨 소리인지 이해가 잘 안가도 좋습니다. 지금부터 함수를 기반으로 한 Mediator 패턴을 구현을 통해 세부적인 내용을 까발려 드리겠습니다.

함수 기반 Mediator 패턴 구현

몇 년 전부터 MVVM(Model View ViewModel) 패턴 없는 GUI 응용프로그램 개발은 데스트탑, 웹, 모바일 모두 생각하고 싶지 않습니다. MVVM에서 Mediator 패턴 기반의 메신저는 컴포넌트 사이의 결합도를 낮춰주는 아주 유용한 도구죠. Mediator 패턴을 구현하는 방법은 여러가지가 있지만 개체지향 프로그래밍과 함수형 프로그래밍 모두 어느 정도 가능한 언어에서 인터페이스를 사용하기보다는 함수를 사용할 수 있도록 구현하는 것이 사용성 측면에서 유연하고 코드 가독성을 높여주기도 합니다.

public class Subscriber : IRecipient
{
    public Subscriber()
    {
        Messenger.Instance.Register(this);
    }

    public void OnMessage(string message)
    {
        Console.WriteLine(message);
    }
}

인터페이스를 사용한 구현 예시. 동작하지 않을 수 있습니다.

public class Subscriber
{
    public Subscriber()
    {
        Messenger.Instance.Subscribe(Console.WriteLine);
    }
}

함수를 사용한 구현 예시. 동작하지 않을 수 있습니다.

하지만 이런한 편리함 뒤에는 참조 관리의 막중한 책임이 따릅니다. 우선 강한 참조를 사용한 메신저에서 발생할 수 있는 문제를 살펴보겠습니다.

강한 참조 기반 메신저

강한 참조를 사용하는 단순한 메신저는 콜백 대리자 목록 관리로 구현될 수 있습니다.

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

    private List<Action<string>> _callbacks = new List<Action<string>>();

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

    public void Unsubscribe(Action<string> callback)
    {
        _callbacks.Remove(callback);
    }

    public void Publish(string message)
    {
        foreach (var callback in _callbacks)
        {
            callback.Invoke(message);
        }
    }
}

이 경우는 이벤트를 사용하는 것과 방식이 유사한데(이벤트는 대리자가 목적에 맞게 캡슐화 된 것이고 모든 대리자 인스턴스는 다중 캐스팅 대리자이며 결국 다중 캐스팅 대리자는 대리자 목록을 관리하므로) 따라서 앞서 언급한 이벤트를 잘 못 사용하면 발생하는 메모리 누수가 그대로 발생할 수 있습니다. 다음 구독자 클래스를 보시죠.

public class Subscriber
{
    private string _name;

    public Subscriber(string name)
    {
        _name = name;
        Messenger.Instance
                 .Subscribe(m => Console.WriteLine("{0} received \"{1}\"", _name, m));
    }

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

강조된 줄의 코드를 보면 람다를 이용해 메시지 구독 콜백을 메신저에 등록합니다. 그러면 이때 람다 함수는 컴파일러에 의해 어떻게 처리될까요? 바로 Subscriber 클래스의 메서드로 구현됩니다. 이때 _name 필드에 접근하려면 this 참조가 필요하기 때문에 인스턴스 메서드가 되죠.

[CompilerGenerated]
private void <.ctor>b__0(string m)
{
    Console.WriteLine("{0} received \"{1}\"", this._name, m);
}

즉, 메신저에 등록되는 대리자는 컴파일러에 의해 생성된 Subscriber 클래스의 메서드와 Subscriber 개체의 참조를 가지게 됩니다. 결국 메신저로부터 Subscriber 개체로의 개체 그래프 탐색이 가능해지고 메신저 개체가 생존하는 한 메시지를 구독하는 모든 Subscriber 개체들은 가비지 컬렉션 대상이 되지 않습니다. 직접 확인해 보겠습니다.

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

        Messenger.Instance.Publish("Hello");

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

        Messenger.Instance.Publish("World");
    }
}

[소스코드] [실행]

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

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

이름이 's2'Subscriber 개체는 생성은 되었지만 이 개체에 대한 참조 변수는 만들어지지 않았습니다. 그래서 Main 메서드 코드만 보면 이 개체는 가비지 컬렉션에서 생존하지 못할 것 처럼 보입니다. 하지만 실행 결과를 보면 살아남아서 두 번째 메시지를 수신했습니다. 이유는 앞서 설명한 것처럼 내부적으로 메신저 인스턴스로부터 참조 연결 되어있기 때문입니다. 이런 상황이 반복되면 메모리 누수 뿐만 아니라 메시징 성능 저하까지 야기할 수 있습니다. 프로그래머가 CLR 가비지 컬렉션과 C#의 람다 구현에 대한 이해가 부족하다면 이 문제는 계속 ‘CLR 메모리 누수’ 미신으로 남을지도 모릅니다.

마무리

메시지 구독 행위가 개체 생명주기에 영향을 주는 것은 의도된 것일 수도, 의도되지 않은 것일 수도 있겠지만 자주 사용되는 메신저 패키지들이 약한 참조를 기반으로 하는 것은 의도되지 않은 경우가 많기 때문이라고 해석할 수 있을지도 모르겠네요. 다음 포스트에서는 약한 참조와 약한 참조를 사용한 메신저 구현 방법을 살펴보고 메신저를 제공하는 몇 가지 메신저 패키지들을 분석하겠습니다.

Advertisements

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

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

답글 남기기

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

WordPress.com 로고

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

Twitter 사진

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

Facebook 사진

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

Google+ photo

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

%s에 연결하는 중