본문 바로가기

C#

WPF MVVM 패턴, 왜 사용할까?

WPF를 사용하는 개발자라면, UI 컨트롤 간 상호작용을 어떻게 효율적으로 처리할 수 있는지에 대해 고민해보셨을 것입니다. 특히, 한 컨트롤의 이벤트가 다른 컨트롤의 상태를 변경해야 할 때, 어떻게 이를 처리해야 할지가 문제입니다. WPF에서는 이를 MVVM( Model-View-ViewModel )패턴을 활용하여 해결하라고 권장하고 있습니다. 이 글에서는 MVVM 패턴을 사용하는 이유에 대해 알아보겠습니다.

 

WPF의 XAML와 코드 비하인드

다음은 간단한 예제 코드입니다. 버튼을 클릭하면, 텍스트 박스에 준비된 텍스트가 표시됩니다.

// XAML
<StackPanel>
    <TextBox Name ="txtBox" Width ="200" HorizontalAlignment="Left"/>
    <Button Content ="Click Me" Name ="button" HorizontalAlignment = "Left" Click = "button_Click" />
</StackPanel>

// Code-behind
private void button_Click(object sender, RoutedEventArgs e)
{
	txtBox.Text = "You have just clicked the button";
}

 

WPF에서는 XAML과 코드 비하인드가 연결되어 있습니다. 예를 들어, XAML에서 정의된 'txtBox' 컨트롤은 코드 비하인드에서 'txtBox'라는 이름을 통해 직접 참조하고 제어할 수 있습니다. 이는 WPF의 XAML 코드가 컴파일 과정에서 .NET 코드로 변환되면서 가능해집니다. 이 변환 과정에서 XAML에 정의된 각 컨트롤은 해당 클래스의 필드로 생성되고, 이 필드들은 코드 비하인드에서 접근 가능합니다.

 

이를 통해 버튼의 클릭 이벤트에서 TextBox의 Text 속성을 변경할 수 있습니다. 이때 TextBox와 버튼이 같은 StackPanel 안에 있지 않아도, 동일한 이름을 가진 TextBox를 찾아서 Text를 변경할 수 있습니다.

 

WPF에서 XAML과 코드 비하인드의 상호 작용은 강력하지만, 몇 가지 주의사항이 있습니다. XAML에서 정의된 컨트롤에 대한 직접적인 참조는 편리해 보일 수 있지만, 이 방식은 특히 대규모 응용 프로그램에서 문제를 일으킬 수 있습니다. XAML과 코드 비하인드 간의 강한 결합은 시스템의 유연성을 저하시키고, 코드의 복잡성을 증가시킵니다. 이는 특히 컨트롤들이 서로 다른 페이지나 윈도우에 걸쳐 있을 때 더욱 두드러집니다. 

 

컨트롤간 직접 참조의 문제점

  1. 코드의 가독성: 여러 컨트롤들이 서로를 참조하면서 코드가 복잡해지고 이해하기 어렵게 됩니다. 이로 인해 유지보수가 어려워질 수 있습니다.
  2. 재사용성: 특정 컨트롤이 다른 컨트롤에 의존하게 되면, 그 컨트롤을 다른 곳에서 재사용하기 어렵게 됩니다.
  3. 테스트 용이성: UI 컨트롤이 서로를 직접 참조하게 되면, 해당 컨트롤의 로직을 독립적으로 테스트하기 어렵게 됩니다.

 

위에서 본 코드를 다시 살펴보겠습니다.

// Code-behind
private void button_Click(object sender, RoutedEventArgs e)
{
	txtBox.Text = "You have just clicked the button";
}

 

위의 코드에서 button_Click 이벤트 핸들러는 txtBox에 직접적으로 의존하고 있습니다. 이로 인해 이 이벤트 핸들러를 다른 텍스트 박스와 함께 재사용하기 어렵습니다. 또한, 이 로직을 테스트하려면 실제 UI 컨트롤인 txtBox가 필요하므로, UI로부터 독립적으로 테스트하기 어렵습니다.

 

MVVM 패턴 적용

MVVM 패턴을 적용하면, 위와 같은 직접적인 컨트롤 참조를 피하면서, UI와 로직을 분리할 수 있습니다. 이는 재사용성과 유지보수성을 크게 향상시킵니다. MVVM 패턴을 적용했을 때의 예시를 살펴보겠습니다.

 

// XAML
<Grid> 
    <TextBox Text="{Binding Message}" />
    <Button Command="{Binding ButtonClickCommand}" Content="Click Me" />  
</Grid>

// ViewModel
public class MyViewModel : INotifyPropertyChanged
{
    private string _message;
    public string Message
    {
        get { return _message; }
        set
        {
            _message = value;
            OnPropertyChanged("Message");
        }
    }

    public ICommand ButtonClickCommand => new RelayCommand(ButtonClick);

    private void ButtonClick()
    {
        Message = "Button was clicked!";
    }

    // INotifyPropertyChanged 구현은 생략
}

 

위의 코드에서는 MyViewModel이라는 ViewModel 클래스를 정의하고, Message라는 속성과 ButtonClickCommand라는 커맨드를 정의했습니다. Message 속성은 텍스트 박스의 Text 속성에, ButtonClickCommand는 버튼의 Command 속성에 바인딩되어 있습니다.

이렇게 하면, 버튼 클릭 시 ButtonClick 메서드가 호출되고, Message 속성이 변경되어 텍스트 박스의 텍스트가 업데이트됩니다. 이 로직은 textBox1이라는 특정 UI 컨트롤에 의존하지 않으므로, 다른 텍스트 박스와 함께 재사용하기 쉽습니다.

또한, MyViewModel은 UI에 의존하지 않으므로, UI와 독립적으로 테스트할 수 있습니다. 예를 들어, ButtonClick 메서드를 호출하고 Message 속성이 올바르게 변경되는지 확인하는 단위 테스트를 작성할 수 있습니다. 이러한 방식으로 MVVM 패턴은 코드의 재사용성과 테스트 용이성을 향상시킵니다.

 

데이터 흐름 비교

직접 참조와 MVVM 패턴을 사용할 때의 데이터 흐름은 다음과 같습니다.

 

직접 참조

  1. 사용자가 버튼 컨트롤을 클릭합니다.
  2. 이에 따라 버튼 클릭 이벤트 핸들러가 실행됩니다.
  3. 이벤트 핸들러 내부에서 텍스트박스 컨트롤의 Text 속성이 직접 변경됩니다.
  4. 변경된 Text 속성에 따라 UI가 업데이트됩니다.

 

MVVM

  1. 사용자가 버튼 컨트롤을 클릭합니다.
  2. 이에 따라 바인딩된 커맨드(ButtonClickCommand)가 실행됩니다.
  3. 커맨드 실행에 따라 ViewModel 내부의 메서드(ButtonClick)가 호출됩니다.
  4. 메서드 내부에서 ViewModel의 Message 속성이 변경됩니다.
  5. Message 속성의 변경이 INotifyPropertyChanged 인터페이스를 통해 UI에 알려집니다.
  6. 알림을 받은 UI는 Text 속성에 바인딩된 Message 속성의 변경을 반영하여 자동으로 업데이트됩니다.

 

직접 참조 방식은 이벤트 핸들러 내에서 UI 컨트롤을 직접 조작하는 반면, MVVM 패턴은 데이터 바인딩을 통해 UI와 로직을 분리합니다. 결과적으로, MVVM은 더 깔끔하고 유지보수가 용이한 코드 구조를 제공합니다.

 

ViewModel의 독립적인 테스트

MVVM 패턴에서 ViewModel은 UI에 의존하지 않으므로, UI와 독립적으로 테스트할 수 있습니다.

MyViewModel의 ButtonClick 메서드와 Message 속성을 테스트하는 단위 테스트를 작성해 보겠습니다. 아래는 NUnit 테스트 프레임워크를 사용한 예시입니다.

[Test]
public void ButtonClick_UpdatesMessage()
{
    // Arrange
    var viewModel = new MyViewModel();

    // Act
    viewModel.ButtonClickCommand.Execute(null);

    // Assert
    Assert.AreEqual("Button was clicked!", viewModel.Message);
}

 

위의 테스트 코드에서는 먼저 MyViewModel의 인스턴스를 생성(Arrange)합니다. 그런 다음 ButtonClickCommand를 실행(Act)하고, 이에 따라 Message 속성이 올바르게 업데이트되는지 확인(Assert)합니다.

 

위의 테스트 코드에서는 어떠한 UI 컨트롤도 포함되지 않았습니다. 이러한 방식으로 ViewModel의 메서드와 속성을 개별적으로 테스트할 수 있습니다. 이는 UI 테스트에 비해 빠르고, 더욱 견고하며, 테스트의 범위를 좁혀 테스트의 복잡성을 줄일 수 있습니다.

 

결론적으로, MVVM 패턴은 View와 ViewModel 간의 결합도를 낮추고, 데이터 바인딩과 이벤트 알림을 통해 유연한 상호작용을 가능하게 합니다. 이는 애플리케이션의 확장성과 유지보수성을 크게 향상시키며, 개발자가 더 견고하고 효율적인 코드를 작성할 수 있게 돕습니다.