목공책 하나 들이셔요~

2014년 4월 9일 수요일

[JavaFX] 프로퍼티와 바인딩 이해하기

Java의 프러퍼티는 JavaBeans에서 부터 시작된 오래된 개념입니다. 그런데 JavaFX에서 이를 확장하여 바인딩과 리스너 개념을 추가했습니다. 바인딩을 통해 UI 컨트롤과 데이터 모델 간의 Seamless한 연동이 가능해 졌습니다. 깔끔한 소스가 특징이지만 직관적이지 않은 코드로 인해 읽기 어려운 단점이 있어 관련 내용을 정확히 이해하는 것이 좋습니다. Oracle에서 제공하는 Using JavaFX Properties and Binding 기사를 번역했습니다. 원문은 다음 링크를 확인하세요.
http://docs.oracle.com/javafx/2/binding/jfxpub-binding.htm


개요

수년 동안 Java 프로그래밍 언어는 객체의 프로퍼티를 구현하기 위해 자바빈즈(JavaBeans) 컴포넌트 아키텍쳐를 사용해 왔습니다. 자바빈즈 모델은 API와 디자인 패턴으로 구성되어 있는데, 이는 Java 어플리케이션 개발자들과 개발 도구에 광범위하게 적용되고 이해되어 온 것들입니다. JavaFX 역시 이 검증된 자바빈즈 모델을 사용하여 프로퍼티를 제공합니다. 하지만 보다 확장되고 개선되었습니다.

JavaFX의 프러퍼티는 주로 바인딩(binding)을 위해서 사용됩니다. 바인딩은 두개의 변수(variable)간의 강력한 직접적인 관계를 표현하는 용어입니다. 어떤 객체들이 바인딩 되었다고 한다면 하나의 객체를 변경하는 것이 다른 객체에 자동적으로 영향을 미치게 됩니다. 이 기능은 어플리케이션을 개발하는데 매우 유용합니다.

예를 들어서 요금 청구서를 추적하는 프로그램을 개발한다고 합시다. 그날의 일일 총 매출은 그날의 요금 청구서 금액을 모두 합친 것입니다. 그런데 어떤 청구서 하나의 금액이 변경되었다면 자동적으로 총 매출도 변경되어야 합니다. 만일 총 매출과 요금 청구서 간에 바인딩 되어 있다면 가능한 일입니다.

더 실질적인 예는 JavaFX와 같은 툴로 UI를 개발하는 경우 입니다. UI 컨트롤과 여기에 보여질 내용을 담은 어플리케이션 데이타간에 주로 바인딩을 합니다. 이렇게 하면 TextField에서 입력한 값은 자동으로 바인딩된 변수로 저장이 되고, 변수의 값을 변경하면 자동으로 UI 컨트롤의 표시 내용이 바뀌게 됩니다.


바인딩은 하나 혹은 그 이상의 소스를 갖게 되는데 이를 의존 객체(dependencies)이라고 합니다. 바인딩은 의존 객체들의 변경 사항을 관찰(observe) 하고 변경 사항이 감지되면 자동으로 의존 객체로부터 데이타를 수집하여 자신을 변경합니다.

바인딩 API는 두개의 큰 범주로 나눌 수 있습니다.

1. 고수준 API : 일반적으로 사용되며 간단한 방법으로 바인딩을 구현할 수 있습니다. 고수준 API 문법은 배우기 쉽고 사용하기 쉽습니다. 가장 대표적인 예는 NetBeans IDE에서 사용하는 자동 코드 완성(code completion) 기능입니다.

2. 저수준 API : 고수준 API에 만족하지 못하는 뛰어난 개발자들이 사용할 수 있으며 매우 유연합니다. 저수준 API는 실행 속도가 빠르고 메모리를 적게 사용하는 장점이 있습니다.

프러퍼티에 대해 이해하기

앞서 언급했듯이 JavaFX의 프러퍼티는 잘 알려진 자바빈즈 컴포넌트 아키텍쳐의 프러퍼티 모델에 기반하고 있습니다. 이 절에서는 이게 무슨 의미인지 살펴 보고, 이 프러퍼티가 JavaFX에 어떻게 사용되는지 설명 드리겠습니다.

Java 프로그래밍 언어는 객체(object)안에 데이타를 품을 수 있게 되어 있습니다. 그리고 당신이 정의하는 메쏘드의 이름 짓기에 대한 강제적인 규칙은 없습니다. 예를 들어 Person이라는 클래스를 정의하였고 성과 이름이라는 필드를 가지고 있다고 해 봅시다. 이름짓기에 대한 관행이 없다면 프로그래머 마다 이 성과 이름을 얻기 위한 다른 이름의 메쏘드를 정의할 겁니다. 예를 들어 read_first(), firstName(), getFN() 등등으로 말이죠. 이렇게 중구난방으로 이름을 짓는다고 프로그램이 안도는 것은 아닙니다만 이 소스코드를 처음 보는 다른 개발자들에게 큰 혼란을 줄 것이라는 것은 분명합니다.

자바빈즈 컴포넌트 아키텍쳐는 간단한 이름짓기 규칙을 정의함으로서 이 문제를 해결 했습니다. 자바빈즈 프로그래밍에서는 이름과 성을 얻기 위한 메쏘드의 형태를 다음과 같이 정의합니다.
public void setFirstName(String name) { ... }
public String getFirstName() { ... }
public void setLastName(String name) { ... }
public String getLastName() { ... }
이런 일관된 명명 규칙은 사람에게도 혼란을 줄여주지만, NetBeans IDE와 같은 편집 도구에게도 많은 것을 할 수 있게 해 줍니다. 자바빈즈의 용어로 Person 객체는 firstName과 lastName이라는 프러퍼티를 가지고 있는 겁니다.

자바빈즈 모델은 이런 단순한 프러퍼티 외에도 복잡한 프러퍼티 모델과 이벤트 전달 시스템과 이를 지원하기 위한 클래스들을 지원합니다. 이런 지원 클래스들은 모두 java.beans 패키지 아래에 있습니다. 그러므로 자바빈즈를 배운다는 것은 이름짓기 규칙과 관련된 API를 배우는 것이라고 할 수 있습니다. (자바빈즈에 대해 더 자세히 알고 싶으면 이 글을 참조하세요)

비슷하게 JavaFX 프러퍼티를 이해하기 위해서는 새로운 몇개의 API와 이름 짓기 관행을 익혀야 합니다. JavaFX에서는 이제 흥미롭게 배우게 될 새로운 프러퍼티들로만 클래스를 구성할 수 있습니다. 아래 코드는 JavaFX 프로퍼티를 사용하는 패턴에 대해 간다하게 보여 줍니다. 이 클래스의 이름은 Bill 이고 amountDue라는 하나의 프러퍼티를 가집니다.

package propertydemo;

import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
 
class Bill {
 
    // Define a variable to store the property
    private DoubleProperty amountDue = new SimpleDoubleProperty();
 
    // Define a getter for the property's value
    public final double getAmountDue(){return amountDue.get();}
 
    // Define a setter for the property's value
    public final void setAmountDue(double value){amountDue.set(value);}
 
     // Define a getter for the property itself
    public DoubleProperty amountDueProperty() {return amountDue;}
}

javafx.beans.DoubleProperty 클래스의 객체인 amountDue는 private으로 정의되어 바깥 세상으로 부터 보호되고 있습니다. 이건 Java 혹은 JavaBeans 어플리케이션의 표준적인 관행입니다. DoubleProperty 클래스는 double이라는 Java의 기본 타입을 감싼 래퍼 클래스이며 double이라는 값을 보관하고 몇몇 추가적인 기능도 함께 제공합니다. javafx.beans.property 패키지에서 정의되는 JavaFX 프러퍼티들은 모두 관측(observability)와 바인딩 기능을 제공합니다.

프러퍼티 메쏘드의 이름 짓기 규칙은 다음과 같습니다.

  • getAmountDue() 메쏘드는 amountDue 프러퍼티가 가지고 있는 현재 값을 리턴하는 표준 getter 입니다. 관행적으로 이 메쏘드는 final로 선언됩니다. 이 메쏘드의 리턴값은 DoubleProperty가 아니라 double임에 유의하세요.
  • setAmountDue(double) 메쏘드 역시 final로 선언되며 프러퍼티의 값을 설정하는 표준 setter 입니다. 읽기 전용 프러퍼티일 경우 이 setter는 없을 수도 있습니다. setter의 인자 역시 기본 타입인 double임에 유의하세요.
  • 마지막으로 amountDueProperty() 메쏘드는 실제 프러퍼티를 얻는 getter 입니다. 이를 위해 새로운 관행을 정의하는데 프러퍼티의 이름은 amountDue 뒤에 "Property"라는 접미어를 붙이는 겁니다. 리턴 타입은 프러퍼티 자체 (DoubleProperty) 입니다.

JavaFX로 GUI 어플리케이션을 만들 때 다루는 클래스들을 보면 이런 JavaFX 프러퍼티 관행을 지키는 것들을 볼 숭 씨습니다. 예를 들어 javafx.scene.shape.Rectangle 클래스는 arcHeight, arcWidth, height, width, x, y 등의 프러퍼티를 가지고 있습니다. 이 프러퍼티들은 JavaFX 프러퍼티의 관행에 따라 이름 지어진 메쏘드들을 제공합니다. 예를 들어서 arcHeight 프러퍼티의 경우 getArcHeight(), setArcHeight(double), arcHeightProperty() 메쏘드 들이 있습니다.

(아래 그림은 Rectangle의 Javadoc인데 Property라는 별도의 항목이 있음을 볼 수 있습니다)


이제 당신은 프러퍼티의 값이 변경될 때 마다 알 수 있는 변경 리스너(change listener)를 다음과 같이 달 수 있습니다.

package propertydemo;
 
import javafx.beans.value.ObservableValue;
import javafx.beans.value.ChangeListener;
 
public class Main {
 
    public static void main(String[] args) {
 
      Bill electricBill = new Bill();
 
      electricBill.amountDueProperty().addListener(new ChangeListener(){
        @Override public void changed(ObservableValue o,Object oldVal, 
                 Object newVal){
             System.out.println("Electric bill has changed!");
        }
      });
     
      electricBill.setAmountDue(100.00);
     
    }
}

이 예제를 실행하면 "Electric bill has changed"라는 메시지가 출력될 겁니다. 즉 변경 리스너가 제대로 동작한다는 것이죠.

고수준 바인딩 API 사용하기

고수준 API는 빠르고 쉽게 바인딩을 사용할 수 있게 해 줍니다. 고수준 API는 두 개의 파트로 나뉘는데 Fluent APIBindings 클래스입니다. Fluent API는 다양한 의존 객체에 대한 메쏘드들을 제공합니다. 반면에 Bindings 클래스는 정적 팩토리 메쏘드들을 제공합니다.

먼저 Fluent API를 알아봅시다. 간단한 예로 두개의 정수가 서로 바인딩 되어 있고 이 두 정수는 항상 더해진다고 해 봅시다. 그러므로 아래 예처럼 세개의 변수가 사용됩니다. num1과 num2는 의존 객체이고 sum은 바인딩입니다. 의존 객체의 타입은 IntegerProperty이고 바인딩은 NumberBinding입니다.

package bindingdemo;
 
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.binding.NumberBinding;
 
public class Main {
 
    public static void main(String[] args) {
        IntegerProperty num1 = new SimpleIntegerProperty(1);
        IntegerProperty num2 = new SimpleIntegerProperty(2);
        NumberBinding sum = num1.add(num2);
        System.out.println(sum.getValue());
        num1.set(2);
        System.out.println(sum.getValue());
    }
}

이 코드는 두개의 의존 객체(1과 2)를 바인딩하여 그 합인 "3"을 출력합니다. 그러고 나서 num1의 값을 2로 바꾼 뒤에 다시 sum을 출력하는데 이때는 "4"가 출력됩니다. 이로서 바인딩이 제대로 동작함을 알 수 있습니다.

비슷하게 Bindings 객체를 이용하여 똑같은 결과를 만들 수 있습니다.

package bindingdemo;
 
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.binding.NumberBinding;
import javafx.beans.binding.Bindings;
 
public class Main {
 
    public static void main(String[] args) {
       IntegerProperty num1 = new SimpleIntegerProperty(1);
       IntegerProperty num2 = new SimpleIntegerProperty(2);
       NumberBinding sum = Bindings.add(num1,num2);
       System.out.println(sum.getValue());
       num1.setValue(2);
       System.err.println(sum.getValue());
    }
}

이 두 접근법을 섞어 좀 더 복잡한 계산을 해 볼까요?

package bindingdemo;
 
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.binding.NumberBinding;
import javafx.beans.binding.Bindings;
 
public class Main {
 
    public static void main(String[] args) {
       IntegerProperty num1 = new SimpleIntegerProperty(1);
       IntegerProperty num2 = new SimpleIntegerProperty(2);
       IntegerProperty num3 = new SimpleIntegerProperty(3);
       IntegerProperty num4 = new SimpleIntegerProperty(4);
       NumberBinding total =
         Bindings.add(num1.multiply(num2),num3.multiply(num4));
       System.out.println(total.getValue());
       num1.setValue(2);
       System.err.println(total.getValue());
    }
}

위 코드는 Fluent API인 multiply 메쏘드를 사용하여 곱하고 이 둘을 합하는 것은 Bindings.add를 이용하였습니다. 고수준 API는 다른 타입의 프러퍼티도 연산할 수 있게 해 줍니다. 이는 다음과 같은 Java 프로그래밍 규칙과 동일합니다.

  • 두 피연산자(operand) 중에서 하나가 double 이면, 그 결과도 double 이다.
  • 위 조건이 아니고 두 피연산자 중에서 하나가 float 이면, 그 결과도 float 이다.
  • 위 조건이 아니고 두 피연산자 중에서 하나가 long 이면, 그 결과도 long 이다.
  • 위 조건이 아니라면 그 결과는 integer 이다.

이어서 관측과 무효 리스너 등에 대해서 알아 봅니다.

Observable, ObservableValue, InvalidationListener, ChangeListener

바인딩 API는 객체의 값이 변경(change)되거나 무효(invalid)화 될 때 자동으로 리스너에 통보하도록 하는 일련의 인터페이스 들을 제공하고 있습니다. Observable과 ObservableValue 인터페이스는 값이 변경되었을 때 통보를 하게 되며, InvalidationListener와 ChangeListener 인터페이스는 이 통보를 받게 됩니다. 이 둘의 차이를 살펴보면 ObservableValue는 값을 감싸고 있으며 값이 변경되었을 때 등록된 ChangeListener에게 통보를 하는데 비해서, Observable은 무효화되어 값(value)이 없으며 이를 InvalidationListener로 통보하는 차이가 있습니다.

JavaFX 바인딩과 프러퍼티는 지연된 계산(lazy evaluation)을 지원합니다. 이것은 어떤 값의 변경이 일어났다고 해도 즉시 모든 계산을 다시 하지는 않는다는 의미입니다. 재계산은 값이 요청될 때에나 하게 됩니다. (이렇게 재계산이 필요하지만 지연된 계산으로 인해 값이 정확치가 않을 때 무효화되었다고 하며, InvalidationListener가 불리게 됩니다)

다음 예제는 3개의 bill 객체를 더해서 total에 바인딩하는 것입니다. bill 객체 중 하나의 값이 바뀌게 되면 의존 객체의 값이 변경된 것이므로 total의 재계산이 필요합니다. 하지만 total의 값을 실제로 요청하기 전까지는 실제 재계산을 하지는 않고 대신 InvalidationListener가 호출됩니다.

package bindingdemo;
 
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.binding.NumberBinding;
import javafx.beans.binding.Bindings;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
 
class Bill {
 
    // Define the property
    private DoubleProperty amountDue = new SimpleDoubleProperty();
 
    // Define a getter for the property's value
    public final double getAmountDue(){return amountDue.get();}
 
    // Define a setter for the property's value
    public final void setAmountDue(double value){amountDue.set(value);}
 
     // Define a getter for the property itself
    public DoubleProperty amountDueProperty() {return amountDue;}
 
}
 
public class Main {
 
    public static void main(String[] args) {
 
        Bill bill1 = new Bill();
        Bill bill2 = new Bill();
        Bill bill3 = new Bill();
 
        NumberBinding total =
          Bindings.add(bill1.amountDueProperty().add(bill2.amountDueProperty()),
              bill3.amountDueProperty());
        total.addListener(new InvalidationListener() {
 
            @Override public void invalidated(Observable o) {
                System.out.println("The binding is now invalid.");
            }
        });

        // First call makes the binding invalid
        bill1.setAmountDue(200.00);

        // The binding is now invalid
        bill2.setAmountDue(100.00);
        bill3.setAmountDue(75.00);

        // Make the binding valid...
        System.out.println(total.getValue());

        // Make invalid... 
        bill3.setAmountDue(150.00);

        // Make valid...
        System.out.println(total.getValue());
    }
}

하나의 bill 객체를 변경하면 바인딩된 값은 무효화 됩니다. 그러므로 무효 리스너가 호출됩니다. 하지만 이미 바인딩이 무효화되어 있다면 이후 bill 객체를 수정하더라도 중복해서 무효 리스너가 호출되지는 않습니다. 이어서 total.getValue() 를 호출하게 되면 재계산이 필요하므로 실제 재계산을 하게 되고 값은 유효화 됩니다. 이어서 다시 bill 객체 중 하나를 변경하면 다시 무효 리스너가 호출 되겠지요.

반면에 ChangeListener를 등록하게 되면 재계산을 많이 하게 됩니다.

저수준 바인딩 API 사용하기

고수준 바인딩 API가 당신의 요구사항에 부합하지 않는다면 이를 대신하여 저수준 API를 사용할 수 있습니다. 저수준 API는 프로그래머에 더 많은 유연성을 제공하고 더 나은 성능을 보장합니다.

다음의 예제는 저수준 API의 기본적은 사용 예를 보여 줍니다.

package bindingdemo;
 
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.binding.DoubleBinding;
 
public class Main {
 
    public static void main(String[] args) {
 
        final DoubleProperty a = new SimpleDoubleProperty(1);
        final DoubleProperty b = new SimpleDoubleProperty(2);
        final DoubleProperty c = new SimpleDoubleProperty(3);
        final DoubleProperty d = new SimpleDoubleProperty(4);
 
        DoubleBinding db = new DoubleBinding() {
 
            {
                super.bind(a, b, c, d);
            }
 
            @Override
            protected double computeValue() {
                return (a.get() * b.get()) + (c.get() * d.get());
            }
        };
 
        System.out.println(db.get());
        b.set(3);
        System.out.println(db.get());
    }
}

저수준 API는 바인딩 클래스 중 하나를 계승 받아서 computeValue() 라는 메쏘드를 구현하는 방식입니다. 위의 예에서는 DoubleBinding 이라는 클래스를 계승 받아서 네 개의 프러퍼티를 연산합니다. 생성자에서 super.bind() 메쏘드를 호출하여 네 개의 의존객체를 넘기는 것은 디폴트 무효화 로직을 등록하기 위해서 입니다. 그러므로 이렇게 하면 바인딩이 무효화 되었는지 일일이 확인하지 않아도 됩니다.

이 정도만 알아도 저수준 API에 대해서는 충분히 사용할 수 있습니다.

댓글 없음:

댓글 쓰기