(WPF) 잘못된 바인딩 사용에 의한 메모리 누수

 

정성태님 블로그 “C# - 닷넷 응용 프로그램에서 메모리 누수가 발생할 수 있는 패턴”에 대한 글을 보던 중 처음 접한 내용을 보았는데.. 단순한 바인딩 처리로 인해 메모리 누수 가능성이 존재 한다는 것이다.

이 와 관련해서 정보를 더 찾던중 jetbrains 블로그 내용에서도 같은 정보를 발견 했다.

(위 내용은 [WPF] WPF의 메모리 릭 발생 가능성 with dotMemory 참조)

이번 글에서는 위 내용중 잘못된 바인딩에 관련해서 실제 메모리 누수가 발생 되는지 간단한

예제를 통해 확인해 보려고 한다.

문제

우선.. 간단한 샘플 프로젝트를 다음과 같이 만들어 보았다

[MainWindow.xaml]

<Grid>
        <Button Content="New window open"
                Width="170"
                Height="50"
                Click="Button_Click"/>
<Grid>

[MainWindow.xaml.cs]

private void Button_Click(object sender, RoutedEventArgs e)
{
            Window1 win = new Window1();
            win.Show();
}

새로운 윈도우를 띄우는 단순한 WPF App이다.

Window1은 단순한 바인딩 샘플로 되어 있다.

[Window1.xaml]

<Window x:Class="WpfApp1.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="Window1" Height="450" Width="800">
    <Grid>
        <StackPanel Orientation="Vertical">
            <TextBlock Text="{Binding SampleText, Mode=TwoWay}"
                       FontSize="30"/>
 
            <TextBlock Text="{Binding SampleText, Mode=TwoWay}"
                       FontSize="30"/>
 
            <TextBlock Text="{Binding SampleText, Mode=TwoWay}"
                       FontSize="30"/>
 
            <TextBlock Text="{Binding SampleText, Mode=TwoWay}"
                       FontSize="30"/>
        <StackPanel>
    <Grid>
<Window>

[Window1.xaml.cs]

namespace WpfApp1
{
    /// 
    /// Window1.xaml에 대한 상호 작용 논리
    /// 
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();
 
            this.DataContext = new WindowViewModel();
        }
    }
 
    public class WindowViewModel
    {
        private string _sampleText = "메모리 누수";
 
        public string SampleText
        {
            get => _sampleText;
            set => _sampleText = value;
        }
    }
}

이 처럼 INotifyPropertyChanged구현이 되어 있지 않는 속성이 바인딩될 경우 WPF는

바인딩 처리를 강력한 참조로 바인딩 소스를 관리 하게 된다.

그 이유는 바인딩 처리 되는 객체의 속성 값이 변경 될때 알림을 받기 위해

System.ComponentModel.PropertyDescriptor클래스의 ValueChanged이벤트를 구독하게 되는데</br/> (객체의 값이 변경 되는 경우 SetValue메서드를 통해 OnValueChanged이벤트로 발생된다.)

이를 위해 내부적으로 런타임시 PropertyDescriptor의 리스트를 관리하는 PropertyDescriptorCollection을</br> Hashtable로 관리하기 때문이다.

위 코드를 빌드하고 메모리 프로파일로 확인해보면 실제 위 내용에 대한 참조 형식을 확인 할 수 있다.

#첫번째 스냅샷

20200925053629_image

#두번째 스냅샷 - Window1 Open

20200925053801_image 20200925053813_image

메모리의 힙 구조를 살펴보면 새로운 Window가 열리면서 WindowViewModel객체가 생성되고

바인딩 소스 관련 객체들이 참조 되고 있는 걸 볼 수 있다. (PropertyDescriptorCollection)

#세번째 스냅샷 - Window1 Close

20200925054126_image 20200925054015_image

Window가 닫혔을때 다시 살펴 보면 여전히 WindowViewModel객체가 남아 있고

바인딩 소스도 그대로 참조 되어 있는 걸 볼 수 있다.

추가로 dotMemory 메모리 프로파일러로 돌려보면 바로 메모리 누수에 대해 알려주는 것을 알 수 있다. 20200925062000_image

해결

위 문제를 해결 하려면 INotifyPropertyChanged를 구현해서 객체 변경에 대해 알림을 통보하도록 처리 하던가
더 이상 해당 바인딩 소스가 필요 없을 때 명시적으로 System.Windows.Data.BindingOperations
ClearBinding메서드를 통해 제거하면 된다.

WindowViewModel클래스에 INotifyPropertyChanged를 구현해 바인딩 처리를 하고 다시 확인 해 보면

[Window1.xaml.cs] - WindowViewModel부분

public class WindowViewModel : INotifyPropertyChanged
{
    private string _sampleText = "메모리 누수 없음";
 
    public string SampleText
    {
        get => _sampleText;
        set
        {
            _sampleText = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SampleText)));
        }
    }
 
    public event PropertyChangedEventHandler PropertyChanged;
}

#첫번째 스냅샷

20200925055858_image

#두번째 스냅샷 - Window1 Open

20200925055938_image 20200925055951_image

#세번째 스냅샷 - Window1 Close

20200925060052_image 20200925060105_image

Window가 닫혔을때 WindowViewModel객체도 사라지고 애초에 바인딩 소스 관련들의 객체 참조도 없는걸 확인 할 수 있다.

※ 위 현상은 바인딩 모드가 OneWay 또는 TwoWay일 경우에만 해당되며, OneTime 또는 OneWayToSource 경우</br>
해당 되지 않습니다.

WPF는 내부 메커니즘이 너무 복잡하기 때문에 이 처럼 간단한 바인딩도 내부 동작 방식을 어느 정도 알 고 있어야
성능향상에 도움이 되고 메모리 누수 현상도 막을 수 있다는걸 다시 한번 깨달았다.</br>

이 외에도 알아야 할 것이 너무 많은 것 같다..ㅠ.ㅠ