티스토리 뷰

WPF .NET

List vs ObservableCollection? part1

kaki104 2022. 3. 8. 10:00
반응형

처음 WPF 개발을 시작하는 분들이 List와 ObservableCollection의 차이점이나 사용 방법에 대해서 알지 못하는 것 같아서 간단하게 포스팅을 하려고 합니다.

 

1. 공통점 vs 차이점

  • 공통점 : 둘다 데이터 목록을 관리합니다.
  • 차이점 : 
    • ObservableCollection은 데이터의 추가, 삭제시 UI에 즉시 반영 됩니다.
    • ObservableCollection이 List에 비해 메모리를 좀더 차지 합니다.
    • 대량의 데이터를 화면에 출력해야하는 경우 ObservableCollection을 사용하면, 성능에 영향을 줄 수 있습니다. (데이터 하나 추가할 때마다 CollectionChanged 이벤트가 계속 발생 -> UI에 반영)
    • 일반적으로 List가 ObservableCollection에 비해 속도가 더 빠릅니다. (하지만, 이것도 데이터의 양에 따라 차이가 미비하거나 없을 수도 있습니다.)
    • LINQ와 사용할 때 ToList()를 이용하면 List로 만들기는 좋은데, ObservableCollection으로 만들어주는 확장 메소드가 없습니다.;;
    •  

2. List를 사용하는 곳과 ObservableCollection을 사용 용도

  • 단순한 데이터 목록에는 대부분 List를 사용합니다. 
    • ex) 검색 결과 조회 화면, 콤보 박스 목록
  • 데이터 목록이 표시되고, 아이템이 추가/삭제되는 마스터 화면등은 ObservableCollection 등으로 만들수 있습니다.
    • ex) 고객 마스터, 상품 마스터 등, 실시간으로 아이템 추가/삭제되는 경우
    • ex) 두개의 ListBox에서 왼쪽 ListBox에서 아이템 하나를 선택해서 오른쪽 ListBox의 아이템에 추가하거나, 오른쪽 ListBox에서 아이템을 선택한 후 제거 버튼을 눌러 왼쪽으로 다시 넣는 경우
그러나, 추가/삭제가 되는 목록이라도, 추가/삭제 후 다시 조회(재조회)를 통해서 처리하는 것이 더 편하다면, List를 사용해도 문제 없습니다. 단, 조회할 데이터의 양이 많다면, 문제가 좀 있을 수는 있겠죠?

3. ObservableCollection을 사용할 때 주의사항

private 필드에 초기값으로 new ObservableCollection<T>를 사용하기 때문에, 뷰모델 내부에서 null로 초기화를 하거나, ToList()를 이용해서 List를 넣지 않고, Clear() 후 Add 혹은 Insert를 이용해서 추가를 하는 것이 좋습니다.

4. List와 ObservableCollection의 비교를 하기 위한 셈플 애플리케이션

MainWindow.xaml

왼쪽은 List 데이터를 바인딩하는 DataGrid이고, 오른쪽은 ObservableCollection 데이터를 바인딩하는 DataGrid입니다.

<Window
    x:Class="ListVsObservableCollection.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:local="clr-namespace:ListVsObservableCollection"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition />
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>
        <StackPanel Margin="5" Orientation="Horizontal">
            <Button
                Width="80"
                Command="{Binding LeftButtonCommand}"
                CommandParameter="Refresh"
                Content="Refresh" />
            <Button
                Width="80"
                Margin="5,0,0,0"
                Command="{Binding LeftButtonCommand}"
                CommandParameter="Add"
                Content="Add" />
            <Button
                Width="80"
                Margin="5,0,0,0"
                Command="{Binding LeftButtonCommand}"
                CommandParameter="Remove"
                Content="Remove" />
        </StackPanel>

        <TextBlock
            Grid.Row="1"
            Margin="5,0,0,0"
            Text="{Binding LeftPeopleTypeName}" />

        <DataGrid
            Grid.Row="2"
            Grid.Column="0"
            Margin="5"
            AutoGenerateColumns="False"
            ItemsSource="{Binding LeftPeople}">
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding Name}" Header="Name" />
                <DataGridTextColumn Binding="{Binding Sex}" Header="Sex" />
                <DataGridTextColumn Binding="{Binding Age}" Header="Age" />
                <DataGridTextColumn Binding="{Binding Address}" Header="Address" />
            </DataGrid.Columns>
        </DataGrid>

        <StackPanel
            Grid.Column="1"
            Margin="5"
            Orientation="Horizontal">
            <Button
                Width="80"
                Command="{Binding RightButtonCommand}"
                CommandParameter="Refresh"
                Content="Refresh" />
            <Button
                Width="80"
                Margin="5,0,0,0"
                Command="{Binding RightButtonCommand}"
                CommandParameter="Add"
                Content="Add" />
            <Button
                Width="80"
                Margin="5,0,0,0"
                Command="{Binding RightButtonCommand}"
                CommandParameter="Remove"
                Content="Remove" />
        </StackPanel>

        <TextBlock
            Grid.Row="1"
            Grid.Column="1"
            Margin="5,0,0,0"
            Text="{Binding RightPeopleTypeName}" />

        <DataGrid
            Grid.Row="2"
            Grid.Column="1"
            Margin="5"
            AutoGenerateColumns="False"
            ItemsSource="{Binding RightPeople}">
            <DataGrid.Columns>
                <DataGridTextColumn Binding="{Binding Name}" Header="Name" />
                <DataGridTextColumn Binding="{Binding Sex}" Header="Sex" />
                <DataGridTextColumn Binding="{Binding Age}" Header="Age" />
                <DataGridTextColumn Binding="{Binding Address}" Header="Address" />
            </DataGrid.Columns>
        </DataGrid>

    </Grid>
</Window>

MainViewModel.cs

기본적인 내용을 가지는 메인뷰모델을 생성합니다.

여기서 주의 깊게 보셔야 하는 부분은 아래 내용입니다.

2개의 프로퍼티는 모두 IList<Person>으로 접근하도록 구성되어 있으며, _rightPeople만 new ObservableCollection<Person>();이 붙어 있는데, 2개로 구성된 프로퍼티(private과 public)의 경우에는 private 필드에 초기값을 주는 것이 일반적인 형태입니다. 프로퍼티에 초기값을 주는 것은 권장됩니다.

 

List<Person>이나 ObservableCollection<Person>이나 둘다 IList 인터페이스를 상속 받기 때문에 선언 및 사용에 문제가 없습니다. 다만, 프로퍼티를 생성할 때에는 정확한 타입명으로 선언을하면, 그 프로퍼티에 넣을 수 있는 값이 한정됩니다. 즉, List형 프로퍼티는 List만 넣을 수 있고, ObservableCollection형 프로퍼티는 ObservableCollection만 넣을 수 있게 됩니다. 처음에는 잘 모르지만, 나중에 사용하면 할수록 불편함을 느끼게 될 것입니다.(특히, ObservableCollection) 그래서, 저는 사용하기 편한 최소 단위 인터페이스를 이용해서 프로퍼티를 생성합니다. 어차피 뷰모델에서는 Add, Remove 등을 IList에 있는 메소드와 기능만 이용해서 데이터 추가/삭제만 하고, CollectionChanged 이벤트를 사용할 일이 별로 없기 때문입니다.

혹시 CollectionChanged 이벤트를 뷰모델 내부에 이벤트 핸들러를 추가해서 사용해야하는 경우라면, 그렇게 사용하셔도 무관하기는 합니다.
가장 작은 목록형 데이터의 타입은 IEnumerable이지만, LINQ를 사용할 수 없기 때문에 잘 사용하지 않습니다.

 

        private IList<Person> _leftPeople;
        /// <summary>
        /// 왼쪽 사람들
        /// </summary>
        public IList<Person> LeftPeople
        {
            get { return _leftPeople; }
            set { SetProperty(ref _leftPeople, value); }
        }
        
        private IList<Person> _rightPeople = new ObservableCollection<Person>();
        /// <summary>
        /// 오른쪽 사람들
        /// </summary>
        public IList<Person> RightPeople
        {
            get { return _rightPeople; }
            set { SetProperty(ref _rightPeople, value); }
        }

최종 소스는 part2 하단에 링크를 추가하도록 하겠습니다.

using Microsoft.Toolkit.Mvvm.ComponentModel;
using Microsoft.Toolkit.Mvvm.Input;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Windows.Input;

namespace ListVsObservableCollection
{
    public class MainViewModel : ObservableObject
    {
        private IList<Person> _persons = new List<Person>
            {
                new Person{ Name = "kaki0104", Sex = true, Age = 11, Address = "Seoul1" },
                //...셈플 데이터
                new Person{ Name = "kaki0143", Sex = false, Age = 150, Address = "Seoul140" },
            };

        /// <summary>
        /// 왼쪽 버튼 커맨드
        /// </summary>
        public ICommand LeftButtonCommand { get; set; }
        /// <summary>
        /// 오른쪽 버튼 커맨드
        /// </summary>
        public ICommand RightButtonCommand { get; set; }

        private Person _selectedLeftPerson;
        /// <summary>
        /// 왼쪽에서 선택된 사람
        /// </summary>
        public Person SelectedLeftPerson
        {
            get { return _selectedLeftPerson; }
            set { SetProperty(ref _selectedLeftPerson, value); }
        }

        private Person _selectedRightPerson;
        /// <summary>
        /// 오른쪽에서 선택된 사람
        /// </summary>
        public Person SelectedRightPerson
        {
            get { return _selectedRightPerson; }
            set { SetProperty(ref _selectedRightPerson, value); }
        }

        private IList<Person> _leftPeople;
        /// <summary>
        /// 왼쪽 사람들
        /// </summary>
        public IList<Person> LeftPeople
        {
            get { return _leftPeople; }
            set { SetProperty(ref _leftPeople, value); }
        }
        /// <summary>
        /// 왼쪽 타입 네임
        /// </summary>
        public string LeftPeopleTypeName
        {
            get { return LeftPeople.GetType().Name; }
        }

        private IList<Person> _rightPeople = new ObservableCollection<Person>();
        /// <summary>
        /// 오른쪽 사람들
        /// </summary>
        public IList<Person> RightPeople
        {
            get { return _rightPeople; }
            set { SetProperty(ref _rightPeople, value); }
        }
        /// <summary>
        /// 오른쪽 타입 네임
        /// </summary>
        public string RightPeopleTypeName
        {
            get { return RightPeople.GetType().Name; }
        }

        /// <summary>
        /// 생성자
        /// </summary>
        public MainViewModel()
        {
            Init();
        }

        /// <summary>
        /// 초기화
        /// </summary>
        private void Init()
        {
            LeftPeople = _persons;
            ((List<Person>)_persons).ForEach(p => RightPeople.Add(p));

            LeftButtonCommand = new RelayCommand<string>(OnLeftButton);
            RightButtonCommand = new RelayCommand<string>(OnRightButton);
        }

        private void OnRightButton(string parameter)
        {
            switch (parameter)
            {
                case "Refresh":
                    break;
                case "Add":
                    break;
                case "Remove":
                    break;
            }
        }

        private void OnLeftButton(string parameter)
        {
            switch (parameter)
            {
                case "Refresh":
                    break;
                case "Add":
                    break;
                case "Remove":
                    break;
            }
        }
    }
}

5. 실행 화면

왼쪽 DataGrid위에 List'1이라고 출력되는 것은 바인딩된 프로퍼티의 타입명입니다.

오른쪽에 DataGrid위에 ObservableCollection'1은 바인딩된 프로퍼티의 타입명입니다.

각 버튼에 코딩을 진행해야 하는데, 전체 내용이 좀 길어지기 때문에, part2에서 계속 진행하도록 하겠습니다.

6. 전체 소스는 part2에서 공개하도록 하겠습니다.

반응형
댓글