소프트웨어 디자인 패턴

디자인 패턴. MVVM (Model-View-ViewModel)

developer-tj 2023. 3. 11. 12:00
반응형

MVVM(Model-View-ViewModel) 패턴은 기본적으로 UI와 밀접하게 연결된 ViewModel 레이어를 추가하여 개발자가 UI와 데이터를 더 쉽게 조작할 수 있도록합니다.

MVVM 패턴은 뷰(View)와 모델(Model) 사이에서 중간 레이어로 ViewModel을 둬, 뷰(View)와 모델(Model)의 결합도를 낮추어 유지보수성을 향상시킵니다.

  • Model: 데이터와 해당 데이터를 조작하는 비즈니스 로직을 담당합니다.
    데이터를 변경하는 주체입니다.
  • View: 유저 인터페이스를 담당합니다.
    데이터를 표시하고, 유저 입력을 받아 ViewModel에 전달합니다.
  • ViewModel: 뷰(View)에서 보여지는 데이터와 이를 조작하는 메서드, 상태를 포함합니다.
    Model과 View 간의 인터페이스 역할을 합니다.

ViewModel은 뷰(View)와 독립적으로 테스트 가능하며, View와의 연결이 끊어지면 다른 View에서 재사용 할 수 있습니다.
또한 ViewModel은 View의 생명주기와 독립적이므로 Android의 경우 화면 회전 등과 같은 구성 변경으로 인한 문제를 해결할 수 있습니다.

MVVM 패턴은 또한 데이터 바인딩(Data Binding)과 같은 고급 기능을 활용하여 뷰(View)와 ViewModel을 자동으로 동기화 할 수 있습니다.
이는 코드의 양을 줄이고 복잡도를 낮추어 개발 속도와 유지보수성을 향상시키는 데 큰 역할을 합니다.


MVVM 패턴은 다음과 같은 특징을 갖습니다.

  1. 양방향 데이터 바인딩(Bidirectional Data Binding)
    MVVM 패턴에서는 View와 ViewModel 간에 데이터를 주고받는 양방향 데이터 바인딩(Bidirectional Data Binding)을 사용합니다.
    이것은 View의 UI 요소에서 변경된 값이 ViewModel에 자동으로 전달되며, ViewModel의 값이 변경되면 View의 UI 요소에 자동으로 반영됩니다.
    이러한 데이터 바인딩은 코드의 양을 줄이고 개발자가 직접 UI 요소와 데이터를 연결하는 작업을 줄여줍니다.
  2. 의존성 주입(Dependency Injection)
    MVVM 패턴에서는 의존성 주입(Dependency Injection)을 사용하여 View와 ViewModel 사이의 의존성을 줄입니다.
    이것은 코드의 복잡성을 줄이고, 단위 테스트를 보다 쉽게 수행할 수 있도록 합니다.
    (코드에서 ViewModel 클래스에서 View 클래스를 가지고 있지 않고, 콜백 함수 등으로 입력받는 방식으로 사용합니다.)
  3. 커맨드(Command)
    MVVM 패턴에서는 커맨드(Command)를 사용하여 View에서 발생하는 이벤트를 ViewModel로 전달합니다.
    이것은 View에서 ViewModel의 메소드를 직접 호출하는 것 대신 커맨드를 통해 ViewModel에게 이벤트를 알리는 것입니다.
    이러한 커맨드는 RelayCommand, DelegateCommand 등의 클래스를 사용하여 구현할 수 있습니다.
  4. 라이브러리 지원
    MVVM 패턴은 다양한 라이브러리들에서 지원하고 있습니다.
    예를 들어, Microsoft사의 WPF와 Silverlight에서는 MVVM 패턴을 지원하며, Google사의 Android에서는 Android Architecture Components를 통해 MVVM 패턴을 지원합니다.
    이러한 라이브러리들은 MVVM 패턴을 보다 쉽게 적용할 수 있도록 하는데 도움을 줍니다.


간단한 MVVM 패턴의 예제 코드입니다.


#include <iostream>
#include <vector>
#include <functional>

// Model
class Data {
public:
    void add(int value) {
        values.push_back(value);
    }

    int get(int index) const {
        return values[index];
    }

    int size() const {
        return values.size();
    }

private:
    std::vector values;
};

// ViewModel
class ViewModel {
public:
    ViewModel(Data& data)
        : data(data)
    {}

    void add(int value) {
        data.add(value);
        if (onDataChanged) {
            onDataChanged();
        }
    }

    void setOnDataChangedFunction(std::function<void()> onDataChangedFunc) {
        onDataChanged = onDataChangedFunc;
    }

private:
    Data& data;
    std::function<void()> onDataChanged = nullptr;
};

// View
class View {
public:
    void showData(const Data& data) const {
        std::cout << "Data: ";
        for (int i = 0; i < data.size(); ++i) {
            std::cout << data.get(i) << " ";
        }
        std::cout << std::endl;
    }

    void bindViewModel(ViewModel& viewModel) {
        viewModel.setOnDataChangedFunction([&]() {
            showData(viewModel.getData());
        });
    }
};

// Main
int main() {
    Data data;
    View view;
    ViewModel viewModel(data);

    view.bindViewModel(viewModel);

    viewModel.add(1);
    viewModel.add(2);
    viewModel.add(3);

    return 0;
}

위의 예제 코드에서 Model은 Data 클래스로 구현되어 있습니다.
Data 클래스는 int형 값들을 저장하는 std::vector를 멤버로 가지고 있으며, 값을 추가하는 add(), 값을 가져오는 get(), 데이터 크기를 반환하는 size() 함수를 제공합니다.

ViewModel은 ViewModel 클래스로 구현되어 있습니다.
ViewModel 클래스는 Data 객체를 받아서, Data에 값을 추가하는 add() 함수를 제공합니다.
add() 함수는 값을 추가한 후, onDataChanged() 함수를 호출하여 데이터 변경 사항을 알립니다.

View는 View 클래스로 구현되어 있습니다.
View 클래스는 Data 객체를 출력하는 showData() 함수를 제공합니다.
또한, bindViewModel() 함수를 사용하여 ViewModel의 onDataChanged() 함수와 View의 showData() 함수를 연결합니다.
이를 통해 ViewModel은 데이터를 처리하면서 View와 직접적인 연결 없이 출력을 요청하게 됩니다.

위 예제 코드에서는 Main 함수에서 Data, View, ViewModel 객체를 생성하여 ViewModel의 add() 함수를 호출하여 데이터를 추가하고, View의 bindViewModel() 함수를 사용하여 ViewModel의 onDataChanged() 함수와 View의 showData() 함수를 연결합니다.
이를 통해 ViewModel은 데이터 변경을 알리면서 View에게 출력 요청을 보내게 됩니다.