바인더블 프로퍼티
바인더블 프로퍼티를 사용하여 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; };
이 클래스는 구독에 대한 정보를 저장하고 수신자에게 구독 정보 변경 사항을 알리기 위한 해당 getter, setter 및 알림 신호를 제공합니다. 또한 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
데이터를 업데이트하기 위해 적절한 신호 슬롯 연결을 수행합니다. 이는 간단하므로 이 부분은 생략하겠습니다.
다음으로 Subscription::priceChanged()
에 연결하여 가격이 변경될 때 UI의 가격을 업데이트합니다.
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 프로퍼티가 같은 문제를 해결하는 데 어떻게 도움이 되는지 살펴봅시다. 먼저 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 클래스를 사용할 수 있습니다.
© 2025 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.