4 minute read

MVVM 패턴이란?


MVVM 패턴은 주로 사용자 인터페이스(UI) 개발에서 사용되는 디자인 패턴으로, 특히 데이터 바인딩을 지원하는 환경에서 효과적입니다.
UI 로직과 비즈니스 로직을 분리하여 코드의 유지보수성과 테스트 용이성을 향상시키는 것을 목표로하는 디자인 패턴으로 각 구성요소는 아래와 같습니다.

  • Model (모델): 애플리케이션의 데이터와 비즈니스 로직을 나타냅니다. 데이터의 구조와 처리 방법을 정의하며, UI와 독립적입니다. 예를 들어, 사용자 정보, 상품 정보, 주문 정보 등이 모델에 해당합니다.
  • View (뷰): 사용자에게 보여지는 UI를 나타냅니다. 사용자 입력을 받고, ViewModel로부터 데이터를 받아 화면에 표시합니다. 버튼, 텍스트 상자, 리스트 등 UI 요소들이 뷰에 속합니다.
  • ViewModel (뷰모델): ViewModel사이의 중개자 역할을 합니다. View에 필요한 데이터를 Model로부터 가져와 가공하고, View의 명령(Command)을 처리하여 Model을 업데이트합니다. View에 표시할 데이터와 View의 동작 로직을 포함합니다. 즉, View를 위한 데이터와 명령을 준비하는 역할을 합니다.



구성요소


  • Model
    • 데이터 객체 (Data Objects): 데이터를 저장하는 객체 (예: Person, Product, Order 클래스).
    • 데이터 접근 로직 (Data Access Logic): 데이터베이스, 네트워크 등에서 데이터를 가져오거나 저장하는 로직.
    • 비즈니스 로직 (Business Logic): 데이터에 대한 규칙 및 처리 로직 (예: 할인 계산, 유효성 검사).
  • View
    • UI 요소 (UI Elements): 버튼, 텍스트 상자, 리스트, 그리드 등.
    • 레이아웃 (Layout): UI 요소의 배치 및 구성.
    • 사용자 인터랙션 (User Interaction): 사용자 입력 (예: 클릭, 키 입력).
    • 데이터 바인딩 (Data Binding): ViewModel의 속성과 UI 요소를 연결하여 데이터 동기화.
  • ViewModel
    • View에 필요한 데이터 (View-Specific Data): View에 표시할 데이터를 Model에서 가져와 적절한 형태로 가공.
    • 명령 (Commands): View의 동작 (예: 버튼 클릭)에 연결된 로직. Model을 업데이트하거나 다른 작업을 수행.
    • 상태 관리 (State Management): View의 상태 (예: 로딩 중, 에러 발생)를 관리.

MVVM 패턴의 동작 방식


  1. View는 ViewModel의 속성(Property)에 데이터를 바인딩합니다. (예: 텍스트 상자의 텍스트 속성을 ViewModel의 name 속성에 바인딩)
  2. 사용자가 View에서 어떤 동작(예: 버튼 클릭)을 수행하면, View는 ViewModel의 명령(Command)을 실행합니다.
  3. ViewModel은 명령을 처리하여 Model을 업데이트합니다.
  4. Model의 데이터가 변경되면, ViewModel은 변경된 데이터를 View에 알립니다. (주로 데이터 바인딩 메커니즘을 통해 자동으로 업데이트됩니다.)
  5. View는 ViewModel로부터 업데이트된 데이터를 받아 화면을 갱신합니다.

MVVM 패턴의 장단점


장점

  • 관심사 분리 (Separation of Concerns): UI 로직, 비즈니스 로직, 데이터 로직을 명확하게 분리하여 코드의 가독성, 유지보수성, 테스트 용이성을 향상시킵니다.
  • 테스트 용이성: ViewModel은 UI와 독립적이므로 단위 테스트가 용이합니다. UI 테스트 없이 비즈니스 로직과 UI 로직을 테스트할 수 있습니다.
  • 재사용성: ViewModel은 여러 View에서 재사용될 수 있습니다. 동일한 데이터를 다른 방식으로 보여주는 여러 View에서 하나의 ViewModel을 공유할 수 있습니다.
  • 개발 효율성 향상: UI 디자이너와 개발자가 병렬적으로 작업할 수 있습니다. UI 디자인은 View에서, 비즈니스 로직은 Model과 ViewModel에서 독립적으로 진행할 수 있습니다.

단점

  • 간단한 UI의 경우 과도한 복잡성을 초래할 수 있습니다. 간단한 UI에서는 MVVM 패턴을 적용하는 것이 오히려 오버 엔지니어링이 될 수 있습니다.
  • ViewModel이 너무 비대해질 수 있습니다. View가 많아지거나 복잡해질수록 ViewModel의 크기가 커질 수 있습니다.

구현


C++은 기본적으로 데이터 바인딩을 직접 지원하지 않기 때문에, MVVM 패턴을 구현하기 위해서는 추가적인 라이브러리나 프레임워크를 사용해야 합니다.

  • Qt Framework: Qt는 크로스 플랫폼 애플리케이션 개발 프레임워크로, Qt Quick/QML을 통해 선언적인 UI 개발과 강력한 데이터 바인딩 기능을 제공합니다. MVVM 패턴 구현에 매우 적합합니다.
  • WTL (Windows Template Library): Windows API 기반의 C++ 라이브러리로, 데이터 바인딩을 위한 기능을 제공하지만, Qt에 비해 기능이 제한적입니다.
  • 직접 구현: 옵저버 패턴, 시그널/슬롯 (Qt의 경우) 등을 이용하여 데이터 바인딩과 유사한 기능을 직접 구현할 수 있지만, 코드의 복잡도가 크게 증가하고 유지보수가 어려워질 수 있습니다.


[Qt문법을 사용한 개념적인 예시]

#include <QObject>
#include <QString>
#include <QDebug>

// Model
class Person : public QObject {
    Q_OBJECT // Qt의 시그널/슬롯 기능을 사용하기 위해 필요
    Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged) // Qt 속성 시스템
public:
    QString name() const { return m_name; }
    void setName(const QString& name) {
        if (m_name != name) {
            m_name = name;
            emit nameChanged(); // 속성 변경 시그널 발생
        }
    }
signals:
    void nameChanged(); // 속성 변경 시그널
private:
    QString m_name;
};

// ViewModel
class PersonViewModel : public QObject {
    Q_OBJECT
    Q_PROPERTY(QString displayName READ displayName NOTIFY displayNameChanged)
public:
    PersonViewModel(Person* person) : m_person(person) {
        connect(m_person, &Person::nameChanged, this, &PersonViewModel::updateDisplayName); // 모델의 시그널에 연결
        updateDisplayName();
    }
    QString displayName() const { return m_displayName; }
private slots: // 슬롯: 시그널에 의해 호출되는 함수
    void updateDisplayName() {
        m_displayName = "Hello, " + m_person->name();
        emit displayNameChanged();
    }
signals:
    void displayNameChanged();
private:
    Person* m_person;
    QString m_displayName;
};

int main(int argc, char *argv[]) {
    QCoreApplication a(argc, argv); // 콘솔 애플리케이션 예시

    Person person;
    person.setName("World");

    PersonViewModel viewModel(&person);

    QObject::connect(&viewModel, &PersonViewModel::displayNameChanged, [](const QString& name){
        qDebug() << name; // 뷰 대신 콘솔에 출력
    });

    person.setName("Qt");

    return a.exec();
}



[직접 구현 방식]

#include <iostream>
#include <string>
#include <vector>
#include <functional> // std::function

// Model
class Person {
public:
    std::string name;
    int age;
};

// Observer 인터페이스
class Observer {
public:
    virtual void update() = 0;
    virtual ~Observer() = default;
};

// ViewModel
class PersonViewModel {
public:
    PersonViewModel(Person& person) : person_(person) {}

    std::string getDisplayName() const {
        return "Name: " + person_.name + ", Age: " + std::to_string(person_.age);
    }

    void setPersonName(const std::string& newName) {
        person_.name = newName;
        notifyObservers();
    }

    void incrementAge() {
        person_.age++;
        notifyObservers();
    }

    void attach(Observer* observer) {
        observers_.push_back(observer);
    }

    void detach(Observer* observer) {
        for (auto it = observers_.begin(); it != observers_.end(); ++it) {
            if (*it == observer) {
                observers_.erase(it);
                return;
            }
        }
    }

private:
    void notifyObservers() {
        for (Observer* observer : observers_) {
            observer->update();
        }
    }

    Person& person_;
    std::vector<Observer*> observers_;
};

// View (콘솔 출력으로 대체)
class ConsoleView : public Observer {
public:
    ConsoleView(PersonViewModel& viewModel) : viewModel_(viewModel) {}

    void display() {
        std::cout << viewModel_.getDisplayName() << std::endl;
    }

    void update() override {
        display();
    }

private:
    PersonViewModel& viewModel_;
};

int main() {
    Person person{"Initial Name", 20};
    PersonViewModel viewModel(person);
    ConsoleView view(viewModel);

    viewModel.attach(&view); // View를 ViewModel에 등록

    view.display(); // 초기 표시

    viewModel.setPersonName("New Name"); // ViewModel을 통해 Model 변경
    // View가 자동으로 업데이트되지 않으므로, update()를 호출해야 함 (간단한 예시를 위한 제약)

    viewModel.incrementAge();

    return 0;
}



Top