티스토리 뷰

반응형

과거의 경험한 내용 중에 대용량의 데이터를 파일이서 잃거나, Rest API를 통해서 전달 받아서, DataGrid나 ListBox에 출력시 UI Freeze 현상과 메모리 사용량이 증가하는 한다는 것을 해결해보려고 노력을 했던 적이 있습니다.

 

2022.04.04 - [WPF .NET] - Async/Await를 사용해서 UI Freeze 해결하기

이전 포스트의 async/await를 이용해도 발생하는 현상인데..

모든 데이터를 조회하고, 컨트롤에 출력하는 시간이 좀 걸리더라도 (화면에 프로그래스 출력하고 카운트 출력하면 그래도 기다릴 수 있을 것이라고 생각하면서..), UI가 부드럽게 동작할 수 있는 방법을 찾아보려고 합니다.

먼저, 대용량 파일을 불러오는 화면을 만들어 보겠습니다.

프로젝트에 여러개의 Window가 있으며, App.xaml.cs에서 시작 윈도우를 지정하는 코드가 있습니다. 참고하시기 바랍니다.

1. 대용량 데이터 파일 확보

Crimes - 2001 to Present | City of Chicago | Data Portal

 

Crimes - 2001 to Present | City of Chicago | Data Portal

 

data.cityofchicago.org

시카고의 2001년 부터 지금까지의 범죄 데이터를 다운로드 받아서 사용해 보도록 하겠습니다.

음..데이터량이 상당한것 같습니다. 최종적으로 1.7GB정도의 csv파일이며, 총 7,514,910 라인입니다.

이 파일은 소스에 포함되지 않기 때문에 여러분이 가지고 계시는 큰 용량의 파일을 사용하시거나 직접 다운로드 받으시기 바랍니다.

2. SampleData.cs

다운로드 받은 시카고 범죄 현황 파일의 데이터를 모델로 변환할 22개의 프로퍼티를 가지고 있는 모델을 만들었습니다.

데이터를 읽어 들인 후 모델로 변환해서 컨트롤에 입력할 것입니다.

    /// <summary>
    /// Sample Data model
    /// </summary>
    public class SampleData
    {
        public string ID { get; set; }
        public string CaseNumber { get; set; }
        public string Date { get; set; }
        public string Block { get; set; }
        public string IUCR { get; set; }
        public string PrimaryType { get; set; }
        public string Description { get; set; }
        public string LocationDescription { get; set; }
        public string Arrest { get; set; }
        public string Domestic { get; set; }
        public string Beat { get; set; }
        public string District { get; set; }
        public string Ward { get; set; }
        public string CommunityArea { get; set; }
        public string FBICode { get; set; }
        public string XCoordinate { get; set; }
        public string YCoordinate { get; set; }
        public string Year { get; set; }
        public string UpdatedOn { get; set; }
        public string Latitude { get; set; }
        public string Longitude { get; set; }
        public string Location { get; set; }
    }

3. MainWindow.xaml

File Open 버튼을 클릭하면 Button_Click 이벤트를 호출하고, 이벤트 내부에서 파일을 선택하고 데이터를 불러올 예정입니다. ListBox와 DataGrid를 각각 추가하고, 모델의 프로퍼티 중 5개만 화면에 표시하도록 만들었습니다.

<Window x:Class="LargeFileReadSample.MainWindow"
        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:LargeFileReadSample"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <StackPanel Orientation="Horizontal">
            <Button Content="File Open" Width="80" Click="Button_Click"/>
        </StackPanel>
        <ProgressBar Grid.Column="1" Grid.ColumnSpan="2" Margin="5,0" IsIndeterminate="True"/>
        
        <ListBox Grid.Row="1" x:Name="listBox">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <TextBlock Text="{Binding ID}"/>
                        <TextBlock Text="{Binding CaseNumber}" Margin="5,0"/>
                        <TextBlock Text="{Binding Date}" Margin="5,0"/>
                        <TextBlock Text="{Binding Block}" Margin="5,0"/>
                        <TextBlock Text="{Binding IUCR}" Margin="5,0"/>
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>

        <DataGrid Grid.Row="1" Grid.Column="1" x:Name="dataGrid">
            <DataGrid.Columns>
                <DataGridTextColumn Header="Id" Binding="{Binding ID}"/>
                <DataGridTextColumn Header="CaseNumber" Binding="{Binding CaseNumber}"/>
                <DataGridTextColumn Header="Date" Binding="{Binding Date}"/>
                <DataGridTextColumn Header="Block" Binding="{Binding Block}"/>
                <DataGridTextColumn Header="IUCR" Binding="{Binding IUCR}"/>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Window>

4. MainWindow.xaml.cs

버튼 클릭시 발생하는 Button_Click이벤트에서는 OpenFileDialog()를 이용해서 파일을 선택하고, 선택된 파일명이 없으면 종료하도록 만들었습니다.

 

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            var dialog = new OpenFileDialog
            {
                Filter = "All files (*.*)|*.*"
            };
            var result = dialog.ShowDialog();
            if (result == false)
            {
                return;
            }
            var fileName = dialog.FileName;
            if (string.IsNullOrEmpty(fileName))
            {
                return;
            }
        }

GetModels 메서드는 파일명을 받아서, 스트림을 열고, 한줄씩 읽어, SampleModel로 변환 후 List에 넣고, 모든 데이터를 읽어서 처리하면, 최종 List를 반환하고 있습니다.

한번에 읽어 오는 ReadToEnd()는 System.OutOfMemoryException에러가 발생됩니다.
        private SampleData GetModelFromString(string item)
        {
            var columns = item.Split(",");
            var model = new SampleData();
            var propertys = model.GetType().GetProperties();
            for (int i = 0; i < propertys.Count(); i++)
            {
                var property = propertys[i];
                property.SetValue(model, columns[i], null);
            }
            return model;
        }

        private IList<SampleData> GetModels(string fileName)
        {
            bool isFirstLine = true;
            IList<SampleData> returnValues = new List<SampleData>();
            using (var reader = new StreamReader(fileName))
            {
                while (!reader.EndOfStream)
                {
                    var line = reader.ReadLine();
                    if (isFirstLine == false && line != null)
                    {
                        var model = GetModelFromString(line);
                        returnValues.Add(model);
                    }
                    else
                    {
                        isFirstLine = false;
                    }
                }
                reader.Close();
            }
            return returnValues;
        }

위의 GetModels를 적용한 Button_Click 메서드 입니다.

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            var dialog = new OpenFileDialog
            {
                Filter = "All files (*.*)|*.*"
            };
            var result = dialog.ShowDialog();
            if (result == false)
            {
                return;
            }
            var fileName = dialog.FileName;
            if (string.IsNullOrEmpty(fileName))
            {
                return;
            }
            var models = GetModels(fileName);
            listBox.ItemsSource = models;
            Debug.WriteLine($"models count : {models.Count}");
        }

5. 첫번째 실행

화면에 프로그래스바 컨트롤을 넣어서 UI Freeze 형상이 발생하는지 여부를 확인합니다.

File Open 버튼 클릭 후 파일 선택

UI Freeze가 발생해서 윈도우 화면 캡처 기능을 이용해서 캡처 했습니다.

오픈이 완료된 화면

CPU : 8%

Memory : 8.7GB

UI Freeze : 약 1분

DataGrid까지 출력하면 너무 오래걸려서 제외 했습니다.

성능 개선을 위해서, 어떤 방법을 사용해야 할까요?

6. Yield를 아시나요?

yield return 문을 사용하여 각 요소를 따로따로 반환할 수 있습니다.

 

Microsoft Doc의 Yield 설명을 참고하면, 스트림 중간에 결과 중 일부를 반환할 수 있는 기능이라는 것입니다.

GetModels에서 파일 데이터를 읽은 후 결과를 returnValues에 저장하고 있다가, 작업이 완료되면 결과를 리턴시키고, 그 데이터를 출력했지만, yield return을 사용하면, 한줄 읽고 반환, 한줄 읽고 반환이 가능하다는 것입니다.

 

시작 윈도우를 YieldWindow.xaml로 변경합니다.

    public partial class App : Application
    {
        public App()
        {
            //StartupUri = new Uri("/MainWindow.xaml", UriKind.RelativeOrAbsolute);
            StartupUri = new Uri("/YieldWindow.xaml", UriKind.RelativeOrAbsolute);

        }
    }

YieldWindow.xaml.cs

Yield를 사용하는 경우 리턴타입은 IEnumerable<T>로 고정되며, 메서드 내부에 데이터를 모아 놓기 위한 리스트가 없고, 한줄읽고, 모델로 변환 후 바로 리턴시켜 버립니다.

        private IEnumerable<SampleData> GetModels(string fileName)
        {
            bool isFirstLine = true;
            using (var reader = new StreamReader(fileName))
            {
                while (!reader.EndOfStream)
                {
                    var line = reader.ReadLine();
                    if (isFirstLine == false && line != null)
                    {
                        var model = GetModelFromString(line);
                        yield return model;
                    }
                    else
                    {
                        isFirstLine = false;
                    }
                }
                reader.Close();
            }
        }

Button_Click 수정

            var models = GetModels(fileName);
            listBox.ItemsSource = models;

7. 두번째 실행

File Open 버튼 클릭 후 파일 선택 후 UI Freeze 발생

오픈이 완료된 화면

CPU : 8%

Memory : 8.7GB

UI Freeze : 약 1분

결과를 한번에 반환하는 방법과 한줄씩 결과를 반환하는 방법에 현재까지 차이는 없는 것 같습니다. 

하지만, 조금더 개선을 할 수 있을 것 같은 느낌같은 느낌이 들고 있습니다. Part2에서 계속 진행하겠습니다.

반응형
댓글