이 페이지에서

바인딩 가능한 속성

바인딩 가능한 속성을 활용하면 C++ 코드를 어떻게 간소화할 수 있는지 보여줍니다.

이 예제에서는 서로 의존하는 서로 다른 객체 간의 관계를 표현하는 두 가지 접근 방식, 즉 시그널/슬롯 연결 기반 방식과 바인더블 속성 기반 방식을 보여드리겠습니다. 이를 위해 구독 비용을 계산하는 구독 서비스 모델을 예로 들어 설명하겠습니다.

연령, 주기, 국가 깃발, 월 요금이 포함된 구독 신청서

시그널/슬롯 접근법을 활용한 구독 시스템 모델링

먼저 Qt 6 이전의 일반적인 구현 방식을 살펴보겠습니다. 구독 서비스를 모델링하기 위해 ` Subscription ` 클래스가 사용됩니다:

class Subscription : public QObject
{
    Q_OBJECT
public:
    enum Duration { Monthly = 1, Quarterly = 3, Yearly = 12 };

    Subscription(User *user);

    void calculatePrice();
    int price() const { return m_price; }

    Duration duration() const { return m_duration; }
    void setDuration(Duration newDuration);

    bool isValid() const { return m_isValid; }
    void updateValidity();

signals:
    void priceChanged();
    void durationChanged();
    void isValidChanged();

private:
    double calculateDiscount() const;
    int basePrice() const;

    QPointer<User> m_user;
    Duration m_duration = Monthly;
    int m_price = 0;
    bool m_isValid = false;
};

이 클래스는 구독에 대한 정보를 저장하고, 구독 정보 변경 사항을 청취자에게 알리기 위한 해당 게터, 세터 및 알림 신호를 제공합니다. 또한 ` User ` 클래스 인스턴스에 대한 포인터를 유지합니다.

구독 요금은 구독 기간을 기준으로 계산됩니다:

double Subscription::calculateDiscount() const
{
    switch (m_duration) {
    case Monthly:
        return 1;
    case Quarterly:
        return 0.9;
    case Yearly:
        return 0.6;
    }
    Q_ASSERT(false);
    return -1;
}

그리고 사용자의 위치에 따라 계산됩니다:

int Subscription::basePrice() const
{
    if (m_user->country() == User::Country::AnyTerritory)
        return 0;

    return (m_user->country() == User::Country::Norway) ? 100 : 80;
}

가격이 변경되면, 변경 사항을 리스너에게 알리기 위해 ` priceChanged() ` 신호가 발송됩니다:

void Subscription::calculatePrice()
{
    const auto oldPrice = m_price;

    m_price = qRound(calculateDiscount() * int(m_duration) * basePrice());
    if (m_price != oldPrice)
        emit priceChanged();
}

마찬가지로, 구독 기간이 변경되면 ` durationChanged() ` 신호가 발송됩니다.

void Subscription::setDuration(Duration newDuration)
{
    if (newDuration != m_duration) {
        m_duration = newDuration;
        calculatePrice();
        emit durationChanged();
    }
}

참고: 두 메서드모두 데이터가 실제로 변경되었는지 확인한 후에만 신호를 발송해야 합니다. 또한 기간이 변경되었을 때 ` setDuration() `은 가격을 재계산해야 합니다.

Subscription 는 사용자가 유효한 국가와 연령을 가지고 있는 경우에만 유효하므로, 유효성은 다음과 같은 방식으로 업데이트됩니다:

void Subscription::updateValidity()
{
    bool isValid = m_isValid;
    m_isValid = m_user->country() != User::Country::AnyTerritory && m_user->age() > 12;

    if (m_isValid != isValid)
        emit isValidChanged();
}

User 클래스는 간단합니다. 사용자의 국가와 연령을 저장하고, 이에 상응하는 게터, 세터 및 노티파이어 신호를 제공합니다:

class User : public QObject
{
    Q_OBJECT

public:
    using Country = QLocale::Territory;

public:
    Country country() const { return m_country; }
    void setCountry(Country country);

    int age() const { return m_age; }
    void setAge(int age);

signals:
    void countryChanged();
    void ageChanged();

private:
    Country m_country { QLocale::AnyTerritory };
    int m_age { 0 };
};

void User::setCountry(Country country)
{
    if (m_country != country) {
        m_country = country;
        emit countryChanged();
    }
}

void User::setAge(int age)
{
    if (m_age != age) {
        m_age = age;
        emit ageChanged();
    }
}

main() 함수에서는 User 및 Subscription 인스턴스를 초기화합니다:

    User user;
    Subscription subscription(&user);

그리고 UI 요소가 변경될 때 user 및 subscription 데이터를 업데이트하기 위해 적절한 신호-슬롯 연결을 수행합니다. 이는 간단하므로 이 부분은 생략하겠습니다.

다음으로, 가격이 변경될 때 UI의 가격을 업데이트하기 위해 Subscription::priceChanged() 에 연결합니다.

    QObject::connect(&subscription, &Subscription::priceChanged, priceDisplay, [&] {
        QLocale lc{QLocale::AnyLanguage, user.country()};
        priceDisplay->setText(lc.toCurrencyString(subscription.price() / subscription.duration()));
    });

또한 구독이 유효하지 않은 경우 가격 표시를 비활성화하기 위해 Subscription::isValidChanged() 에 연결합니다.

    QObject::connect(&subscription, &Subscription::isValidChanged, priceDisplay, [&] {
        priceDisplay->setEnabled(subscription.isValid());
    });

구독 요금과 유효 기간은 사용자의 국가와 연령에 따라 달라지기 때문에, User::countryChanged() 및 User::ageChanged() 시그널에도 연결하여 subscription 을 그에 맞게 업데이트해야 합니다.

    QObject::connect(&user, &User::countryChanged, &subscription, [&] {
        subscription.calculatePrice();
        subscription.updateValidity();
    });

    QObject::connect(&user, &User::ageChanged, &subscription, [&] {
        subscription.updateValidity();
    });

이 방법은 작동하지만 몇 가지 문제가 있습니다:

  • user 와 subscription 의 변경 사항을 모두 제대로 추적하기 위해 신호-슬롯 연결에 많은 상용구 코드가 필요합니다. 가격 계산에 사용되는 의존성 중 하나라도 변경되면, 해당 알림 신호를 발송하고, 가격을 재계산한 뒤 UI에서 업데이트해야 한다는 점을 기억해야 합니다.
  • 향후 가격 계산에 더 많은 종속성이 추가된다면, 신호-슬롯 연결을 더 추가해야 하며, 어떤 종속성이 변경될 때마다 모든 종속성이 제대로 업데이트되도록 해야 합니다. 전체적인 복잡성이 증가하고, 코드 유지보수가 더 어려워질 것입니다.
  • Subscription 와 User 클래스는 신호/슬롯 메커니즘을 사용하기 위해 메타객체 시스템에 의존합니다.

더 나은 방법이 있을까요?

바인더블 속성을 이용한 구독 시스템 모델링

이제 Qt 바인더블 속성 (Bindable Properties )을 사용하여 동일한 문제를 어떻게 해결할 수 있는지 살펴보겠습니다. 먼저, Subscription 클래스와 유사하지만 바인더블 속성을 사용하여 구현된 BindableSubscription 클래스를 살펴보겠습니다:

class BindableSubscription
{
public:
    enum Duration { Monthly = 1, Quarterly = 3, Yearly = 12 };

    BindableSubscription(BindableUser *user);
    BindableSubscription(const BindableSubscription &) = delete;

    int price() const { return m_price; }
    QBindable<int> bindablePrice() { return &m_price; }

    Duration duration() const { return m_duration; }
    void setDuration(Duration newDuration);
    QBindable<Duration> bindableDuration() { return &m_duration; }

    bool isValid() const { return m_isValid; }
    QBindable<bool> bindableIsValid() { return &m_isValid; }

private:
    double calculateDiscount() const;
    int basePrice() const;

    BindableUser *m_user;
    QProperty<Duration> m_duration { Monthly };
    QProperty<int> m_price { 0 };
    QProperty<bool> m_isValid { false };
};

가장 먼저 눈에 띄는 차이점은 데이터 필드가 이제 ` QProperty ` 클래스 내에 묶여 있으며, 노티파이어 시그널(결과적으로 메타오브젝트 시스템과의 종속성도)이 사라지고, 대신 각 ` QProperty `에 대해 ` QBindable `을 반환하는 새로운 메서드들이 추가되었다는 점입니다. ` calculatePrice() ` 및 ` updateValidty() ` 메서드도 제거되었습니다. 이 메서드들이 더 이상 필요하지 않은 이유는 아래에서 살펴보겠습니다.

BindableUser 클래스는 User 클래스와 다음과 같은 점에서 유사하게 다릅니다:

class BindableUser
{
public:
    using Country = QLocale::Territory;

public:
    BindableUser() = default;
    BindableUser(const BindableUser &) = delete;

    Country country() const { return m_country; }
    void setCountry(Country country);
    QBindable<Country> bindableCountry() { return &m_country; }

    int age() const { return m_age; }
    void setAge(int age);
    QBindable<int> bindableAge() { return &m_age; }

private:
    QProperty<Country> m_country { QLocale::AnyTerritory };
    QProperty<int> m_age { 0 };
};

두 번째 차이점은 이러한 클래스의 구현 방식에 있습니다. 우선, ` subscription `과 ` user ` 간의 종속성은 이제 바인딩 표현식을 통해 추적됩니다:

BindableSubscription::BindableSubscription(BindableUser *user) : m_user(user)
{
    Q_ASSERT(user);

    m_price.setBinding(
            [this] { return qRound(calculateDiscount() * int(m_duration) * basePrice()); });

    m_isValid.setBinding([this] {
        return m_user->country() != BindableUser::Country::AnyCountry && m_user->age() > 12;
    });
}

배경에서 바인딩 가능한 속성들은 의존성 변화를 추적하며, 변경 사항이 감지될 때마다 속성의 값을 업데이트합니다. 따라서 예를 들어 사용자의 국가나 연령이 변경되면, 구독 요금과 유효 기간이 자동으로 업데이트됩니다.

또 다른 차이점은 세터가 이제 매우 간단해졌다는 점입니다:

void BindableSubscription::setDuration(Duration newDuration)
{
    m_duration = newDuration;
}

void BindableUser::setCountry(Country country)
{
    m_country = country;
}

void BindableUser::setAge(int age)
{
    m_age = age;
}

세터 내부에서 속성 값이 실제로 변경되었는지 확인할 필요가 없습니다. ` QProperty `가 이미 그 작업을 수행하기 때문입니다. 값이 실제로 변경된 경우에만 종속 속성에 변경 사항이 알림됩니다.

UI에서 가격 정보를 업데이트하는 코드도 단순화되었습니다:

    auto priceChangeHandler = subscription.bindablePrice().subscribe([&] {
        QLocale lc{QLocale::AnyLanguage, user.country()};
        priceDisplay->setText(lc.toCurrencyString(subscription.price() / subscription.duration()));
    });

    auto priceValidHandler = subscription.bindableIsValid().subscribe([&] {
        priceDisplay->setEnabled(subscription.isValid());
    });

bindablePrice() 와 bindableIsValid() 를 통해 변경 사항을 구독하고, 이 속성 중 어느 하나라도 값이 변경되면 가격 표시를 그에 따라 업데이트합니다. 해당 핸들러가 활성화된 동안에는 구독도 유지됩니다.

또한, 복사 시 바인딩에 대해 어떻게 처리해야 할지가 정의되어 있지 않으므로, BindableSubscription 와 BindableUser 의 복사 생성자는 모두 비활성화되어 있다는 점에 유의하십시오.

보시다시피 코드가 훨씬 단순해졌으며, 앞서 언급한 문제들도 해결되었습니다:

  • 신호-슬롯 연결을 위한 상용구 코드가 제거되었으며, 이제 종속성이 자동으로 추적됩니다.
  • 코드의 유지보수가 더 쉬워졌습니다. 향후 의존성을 추가할 때는 해당 바인딩 가능한 속성을 추가하고, 서로의 관계를 반영하는 바인딩 표현식을 설정하기만 하면 됩니다.
  • Subscription 및 User 클래스는 더 이상 메타객체 시스템에 의존하지 않습니다. 물론, 필요한 경우 여전히 메타객체 시스템에 이를 노출하고 Q_PROPERTY를 추가할 수 있으며, C++ 및 QML 코드 모두에서 바인딩 가능한 속성의 이점을 누릴 수 있습니다. 이를 위해 QObjectBindableProperty 클래스를 사용할 수 있습니다.

예제 프로젝트 @ code.qt.io

© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.