잘못된 바인딩 사용에 의한 메모리 누수
정성태님 블로그 "C# - 닷넷 응용 프로그램에서 메모리 누수가 발생할 수 있는 패턴"에 대한 글을 보던 중 처음 접한 내용을 보았는데.. 단순한 바인딩 처리로 인해 메모리 누수 가능성이 존재 한다는 것이다. 이 와 관련해서 정보를 더 찾던중 jetbrains 블로그 내용에서도 같은 정보를 발견 했다. (위 내용은 링크 참조)
이번 글에서는 위 내용중 잘못된 바인딩에 관련해서 실제 메모리 누수가 발생 되는지 간단한 예제를 통해 확인해 보려고 한다. ● 문제우선.. 간단한 샘플 프로젝트를 다음과 같이 만들어 보았다. [MainWindow.xaml]
<Grid> <Button Content="New window open" Width="170" Height="50" Click="Button_Click"/> Grid> | cs |
|
[MainWindow.xaml.cs]
private void Button_Click(object sender, RoutedEventArgs e) { Window1 win = new Window1(); win.Show(); } | cs |
|
새로운 윈도우를 띄우는 단순한 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> | cs |
|
[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; } } } | cs |
|
이 처럼 INotifyPropertyChanged구현이 되어 있지 않는 속성이 바인딩될 경우 WPF는 바인딩 처리를 강력한 참조로 바인딩 소스를 관리 하게 된다. 그 이유는 바인딩 처리 되는 객체의 속성 값이 변경 될때 알림을 받기 위해 System.ComponentModel.PropertyDescriptor클래스의 ValueChanged이벤트를 구독하게 되는데 (객체의 값이 변경 되는 경우 SetValue메서드를 통해 OnValueChanged이벤트로 발생된다.) 이를 위해 내부적으로 런타임시 PropertyDescriptor의 리스트를 관리하는 PropertyDescriptorCollection을 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; } | cs |
|
#첫번째 스냅샷#두번째 스냅샷 - Window1 Open
#세번째 스냅샷 - Window1 Close
Window가 닫혔을때 WindowViewModel객체도 사라지고 애초에 바인딩 소스 관련들의 객체 참조도 없는걸 확인 할 수 있다.
※ 위 현상은 바인딩 모드가 OneWay 또는 TwoWay일 경우에만 해당되며, OneTime 또는 OneWayToSource 경우 해당 되지 않습니다. |
WPF는 내부 메커니즘이 너무 복잡하기 때문에 이 처럼 간단한 바인딩도 내부 동작 방식을 어느 정도 알 고 있어야 성능향상에 도움이 되고 메모리 누수 현상도 막을 수 있다는걸 다시 한번 깨달았다. 이 외에도 알아야 할 것이 너무 많은 것 같다..ㅠ.ㅠ
|