정성태님 블로그 “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로 관리하기 때문이다.
위 코드를 빌드하고 메모리 프로파일로 확인해보면 실제 위 내용에 대한 참조 형식을 확인 할 수 있다.
#첫번째 스냅샷
#두번째 스냅샷 - Window1 Open
메모리의 힙 구조를 살펴보면 새로운 Window가 열리면서 WindowViewModel객체가 생성되고
바인딩 소스 관련 객체들이 참조 되고 있는 걸 볼 수 있다. (PropertyDescriptorCollection)
#세번째 스냅샷 - Window1 Close
Window가 닫혔을때 다시 살펴 보면 여전히 WindowViewModel객체가 남아 있고
바인딩 소스도 그대로 참조 되어 있는 걸 볼 수 있다.
추가로 dotMemory 메모리 프로파일러로 돌려보면 바로 메모리 누수에 대해 알려주는 것을 알 수 있다.
해결
위 문제를 해결 하려면 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;
}
#첫번째 스냅샷
#두번째 스냅샷 - Window1 Open
#세번째 스냅샷 - Window1 Close
Window가 닫혔을때 WindowViewModel객체도 사라지고 애초에 바인딩 소스 관련들의 객체 참조도 없는걸 확인 할 수 있다.
※ 위 현상은 바인딩 모드가 OneWay 또는 TwoWay일 경우에만 해당되며, OneTime 또는 OneWayToSource 경우</br>
해당 되지 않습니다.
WPF는 내부 메커니즘이 너무 복잡하기 때문에 이 처럼 간단한 바인딩도 내부 동작 방식을 어느 정도 알 고 있어야
성능향상에 도움이 되고 메모리 누수 현상도 막을 수 있다는걸 다시 한번 깨달았다.</br>
이 외에도 알아야 할 것이 너무 많은 것 같다..ㅠ.ㅠ