IBM Korea Skip to main content
       IBM 홈    |  제품 & 서비스  |  고객지원 & 다운로드  |  회원가입  

지능형 데이터로 Swing을 간단히!
복잡한 GUI 개발을 쉽게 하기 위해 iData 기법 사용하기

Jonathan Simon
클라이언트측 설계자/Swing 개발자, JPMorganChase and Entology, Inc.
2002년 1월

Swing 아키텍처는 자바 개발자들로 하여금 수많은 데이터를 표시하는 복잡한 화면을 만들 수 있게 해준다. 불행히도 그 데이터를 대규모의 Swing 컴포넌트에 유지하는 것은 악몽의 코딩이 될 수 있다. 이 글에서 Jonathan Simon은 iData 즉 intelligent data라는 기법을 제시한다. 여러분은 iData 아키텍처를 사용해서 여러분 애플리케이션 내에 중앙 데이터 리파지토리를 만들 수 있다. 그렇게 함으로써 여러분은 데이터를 화면에서 보다 완전히 분리할 것이고 더욱 깔끔하고 유지보수하기 쉬운 코드를 만들 수 있을 것이다. 여러분의 시작을 도와 주도록 예제 코드를 갖춘 오픈 소스 툴킷도 나와있다. 이 글을 계속 읽어 나가서 iData 기법에 대해 더 많은 것을 알게 되고 샘플 구현도 보기 바란다.

고급 Swing 아키텍처는 개발자로 하여금 유례없이 복잡한 화면을 설계할 수 있게 해준다. 그러한 화면은 에러가 나기 쉽고 유지보수 하기도 힘든 엄청난 양의 로직을 필요로 하곤 한다. 고급  Swing 컴포넌트 (예를 들면, JTable과 JTree 같은)에서는 좀 더 광범위한 정보가 필요할 때 프로그램 로직이 셀을 기초로 데이터를 보관, 편집, 랜더링하여 종종 어려운 상황이 발생하곤 한다. 지능적 데이터, 즉 고급 정보를 담은 데이터는 고급 애플리케이션을 개발하기 위해 필요한 지식을 제공하는 셀 데이터와 마찬가지로 컴포넌트 모델에 보관될 수 있다. 이 글에서 설명하는 iData 기술은 Model-View-Controller 아키텍처를 유지하면서 지능형 데이터를 Swing 컴포넌트와 통합하기 위한 포괄적인 아키텍처를 구축한다. 이는 데이터 보관, 데이터 조회 간접 접근 및 화면 설정 간접 접근에 대해 지능형 데이터를 사용하는 밀접하게 통합된 간접 접근형 스키마를 통하여 이루어진다. 결과로 나오는 간접 접근 객체들은 복잡한 비즈니스 화면 로직과 복잡성을 최소화한 상호 작용 기능을 구현하기 위한 유연하고 확장성 있는 중앙 저장소를 만들어낸다.

자신의 프로젝트에 iData 아키텍처를 통합하는 개발자들을 도와줄 오픈 소스 iData 툴킷이 나와 있다. 이 툴킷은 간접 접근 층을 정의하는 각종 인테페이스들의 집합 뿐 아니라 기본 구현, 최적화, 사용자 정의 편집기와 변환기들, 그리고 수 많은 예제로 구성되어있다. (참고자료).

iData 기법의 세 가지 층
iData 기법은 다음 세 가지 층으로 구성되어 있다

  • 데이터 보관: iData 기법은 애플리케이션이 DataObjects에 데이터를 보관한다고 가정한다. DataObject는 상응하는 get[FieldName]()과 set[FieldName]() 메소드를 가진 수많은 필드를 포함하고 있는 자바빈 방식의 객체로 느슨하게 정의된다.
  • 컴포넌트를 표시하는 데이터 값 간접 접근: 데이터 간접 접근 층은 DataObject를 포함하고 있는 객체를 정의하는 인터페이스로 구성되어 있다. 이것은 지능형 데이터,즉 iData 층으로도 알려져 있다 (iData 층이 전체적인 아키텍쳐의 명칭인 iData 기법과 혼동되어서는 안된다). iData 층의 인터페이스들은 DataObject 내의 필드에 접근하고 변경하기 위한 일반적인 메소드를 정의한다. 각각의 구체적인 iData 층 클래스는 특정 요구사항에 대해 이 일반적인 접근자와 변환자 메소드를 구현한다. 보통 iData 층의 구현은 DataObject에서 값을 얻고 설정한다. 그러나 예에서도 보겠지만 이러한 간접 접근은 편집 검증, 가상 데이터 및 데이터 장식 등을 포함한 복잡한 로직들을 구현하기 위한 중앙의 보관소를 만든다. iData 레벨은 변경 불가능한 (읽기 전용) 데이터와 변경 가능한(읽기/쓰기) 데이터를 위한 기능으로 더 나누어진다. 이러한 구분은 편집 로직이 필요 없는 복잡한 비편집 데이터와의 인터페이스를 간단하게 하기 위해 만들어졌다.
  • 데이터에 기반하여 편집과 랜더링 컴포넌트를 커스터마이징하는 화면 간접 접근: 지능형 화면, 즉 iDisplay 층은 iData 층과 마찬가지로 간접 접근 방식을 이용하여 지능형 화면을 구현한다. iDisplay 층은 iData 층의 객체들을 편집하고 랜더링하는 컴포넌트에 대한 인터페이스를 정의한다. 이러한 iDisplay 층 커스터마이제이션의 예에는 셀의 배경 색을 바꾸어 에러 상황을 표시하는 것과 iData층의 구현 내용이 자신의 데이터를 편집하기에 가장 적합한 컴포넌트를 결정할 수 있도록 하는 일반적인 편집기를 만드는 것 등을 들 수 있다. iData층과 마찬가지로 iDisplay 층도 변경 불가능한 데이터와 변경 가능한 데이터를 위한 기능으로 나누어진다.

이 세 가지 층은 서로 결합하여 데이터 자체가 아닌 컴포넌트 모델에 덧붙여지는 밀접하게 통합된 간접 접근 객체 세트를 만든다. 이 아키텍처는 Swing에서 Model-View-Controller 아키텍쳐를 유지하는 한편 셀 기반의 기술을 가능하게 한다. 데이터를 조회, 표시, 편집하는 로직은 각 셀 안에 있는 지능형 데이터 객체 내에 캡슐화된다. 그 결과 복잡한 사용자 인터페이스 표시와 상호작용을 구현하기 위한 기능적으로 유연하고 확장성 있는 기법이 만들어진다.

그림 1. iData 기법의 전체 아키텍처 클래스 도표
Class diagram of the complete architecture for the iData technique

다음 섹션들에서는 iData 기법에 대한 아키텍처의 각 층을 살펴볼 것이다. 우리는 이 기술을 보여 주기 위해 가상의 자전거 상점 애플리케이션의 부분들을 만들어 갈 것이다.

DataObjects
위에서 언급했듯이, DataObject는 상응하는 get[FieldName]()과 set[FieldName]() 메소드를 가진 수많은 필드들을 포함하는 자바빈 방식 객체로 정의된다. 데이터 필드들은 보통 비즈니스 영역에 따라 DataObject 내에서 결합된다. 우리의 예인 자전거 상점 애플리케이션은 여러 필드들 (modelName, manufacturer, price, cost 등)과 상응하는 get/set 메소드를 가진 Bicycle이라는 DataObject를 가지고 있다. 자전거 상점에서의 또 다른 잠재적인 DataObject로는 Bicycle과 유사한 필드를 가진 BicycleComponen와 purchasorName, price, dataOfPurchase등의 필드를 가진 Purchase DataObject를 들 수 있다. 다음은 자전거 상점 애플리케이션에서 Bicycle DataObject의 일부를 보여주는 예이다.

Listing 1. 샘플 DataObject


public class Bicycle
{
     //fields
     double price = ...
     String manufacturer = ...
     ...


     //default constructor
     public Bicycle(){}



     //accessors
     public Double getPrice()
     {
          //sometimes its necessary to wrap primitives in related 
          //Object types...
          return new Double(this.price);
     }

     public String getManufacturer()
     {
          return this.manufacturer;
     }


     ...


     //mutators
     public void setPrice(Double price)
     {
          this.price = price.doubleValue();
     }

     public void setManufacturer(String manufacturer)
     {
          this.manufacturer = manufacturer;
     }

     ...

}     

간접 접근: iData 층


위에서 언급했듯이, iData층은 변경 불가능한 데이터와 변경 가능한 데이터를 위한 기능으로 나누어진다. MutableData 인터페이스는 ImmutableData 인터페이스를 확장하기 때문에, 우리는 변경 불가능한 기능을 먼저 검토하겠다.

읽기 전용의 지능형 데이터 (ImmutableIData)를 위한 데이터 간접 접근층

ImmutableData 인터페이스는 iData층의 일부분이다; 이것은 변경 불가능한 iData 간접 접근을 나타내며, 두 가지 메소드와 하나의 권장되는 메소드 오버라이딩으로 구성된다:

  • getData()는 DataObject로부터 정의된 유형의 데이터 값을 반환한다.
  • getSouce()는 DataObject 자신을 반환한다.
  • toString() 메소드를 오버라이딩하면 getData()의 결과를 표시하는 string을 반환한다.

Manufacturer 필드를 위한 ImmutableData 구현 내용을 예로 살펴보자.

Listing 2. 자전거 제조업자를 위한 ImmutableIData 구현



public class BicycleManufacturerIData implements ImmutableIData
{
  //the DataObject
  Bicycle bicycle = null;

  public BicycleManufacturerIData(Bicycle bicycle)
  {
    this.bicycle = bicycle;  //cache the DataObject 
  }

  public Object getSource()
  {
    return this.bicycle; //this simply returns the DataObject
  }

  public Object getData()
  {
    //returns the manufacturer field from the DataObject.  
    //This is the main logical method of the indirection layer.
    return bicycle.getManufacturer(); 
  }

  public String toString()
  {
    //create a safe to String method to avoid null pointer exceptions 
    //while painting...
    Object data = this.getData();
    if (data != null)     
      return data.toString();
    else
      return "";
  }
}

iData 툴킷은 DefaultImmutableData라는 ImmutableData를 구현하는 추상 클래스를 제공한다. 이것은 getData()를 안전하게 반환하기 위해 Object의 toString() 메소드를 오버라이드한다. 우리의 나머지 예들은 iData 층 인터페이스들의 기본 구현 내용을 확장하게 될 것이다. 이러한 기본 구현 내용들은 툴킷에도 포함되어있다.

JTable과의 통합
계속해서 우리의 자전거 상점 예제를 가지고 iData 기술을 JTable에 결합시켜보자. 테이블은 manufacturer, modelName, modelID, price, cost 및inventory에 해당하는 열을 가지고 있다. ImmutableIData 구현의 나머지가 manufacturer iData의 코드를 따라 작성되었다고 가정하자.

JTable DataModel에 실제로 추가된 데이터들은 각각 하나의 DataObject를 포함하고 있는 ImmutableIData의 구현 내용들이다. 하나의 DataObject를 포함하고 있는 iData 층의 구현을 추가한다라는 개념은 앞에서 언급했던 간접 접근 층을 실현한 것이다. .

그림 2. DataObject들을 담고있는 ImmutableIData 구현 내용과 JTable 셀
JTable cells with ImmutableIData implementations containing DataObjects

나는 한번에 한 행 전체를 만들어 내는데 createRow()라는 헬퍼 메소드가 유용하다는 것을 알았다. AbstractTableModel의 서브클래스를 사용할 때 이 행 전체가 모델에 추가될 수 있다. createRow() 메소드는 DataObject를 매개변수로 취하고 특정 테이블에 대해 적절한 ImmutableIData 구현을 인스턴스화한다.

Listing 3. createRow() 메소드


protected Vector createRow(Bicycle bicycle)
  {
    Vector vec = new Vector();
      vec.add(new BicycleModelNameImmutableIData(bicycle));
      vec.add(new BicycleModelIDImmutableIData(bicycle));
      vec.add(new BicycleManufacturerImmutableIData(bicycle));
      vec.add(new BicyclePriceAndCostImmutableIData(bicycle));
      vec.add(new BicycleProfitImmutableIData(bicycle));
      vec.add(new BicycleInventoryImmutableIData(bicycle));
    return vec;
  }

추가적으로, createRow()메소드는 어떤 ImmutableIData 구현 내용을 모델에 두어야 하는지를 판별하는 로직을 위한 중앙의 저장소이다. 이것은 또한 간단한 ImmutableIData 구현을 위해 createRow() 메소드 내에 직접 선언된 익명의 이너 클래스들을 사용하기 위한 클래스 관리의 측면에서도 유용하다.

랜더링 순서
기본 renderer들은 객체의 toString() 메소드를 호출함으로써 그들이 표시할 객체의 문자열 표현을 만든다. 이것은 ImmutableIData 구현 내용이 유용한 toString() 메소드를 가지는 것이 중요한 이유 중 하나이다. 랜더링 시에 renderer는 JTable로부터 ImmutableIData 구현 내용을 받는다. iData를 랜더링하기 위해 toString() 메소드가 호출되어진다. 다음은 전체 랜더링 순서이다.

  • iData 구현상의 toString()
  • iData 구현상의 getData()
  • DataObject상의 get[FieldName]()

그림 3. 랜더링 순서
Rendering sequence

그림 4. 읽기 전용 테이블
Read-only table

동적 영속성
iData의 데이터로 DataObjects를 사용하면 iData 간접 접근에 대한 유연성을 제공할 뿐만 아니라 동적인 영속성을 제공하는 유용한 데이터 간접 접근 층을 추가하기도 한다. 값이 외부에서 수정되는 테이블의 예를 생각해 보자. 보통, 클라이언트는 수정된 값을 보존할 모델 내의 위치를 추정하기 위해 복잡한 로직을 구현해야 할 것이다. iData와 DataObject 간접 접근을 사용하면 새로운 값들이 자동으로 보관되기 때문에 이 로직은 완전히 불필요하게 된다. 이는 동일한 DataObject 인스턴스를 포함하고 있는 여러 개의 iData 객체로 모델을 구성한 결과이다. 한 DataObject의 내부 값이 변경될 때 DataObject 자신은 변경되지 않은 채로 있다. 왜냐하면 모든 iData 객체들은 동일한 인스턴스를 가리키기 때문이다. get 과 set 메소드를 사용하여 DataObject를 다시 조회하면 어떤 수동적인 보관 방법 없이도 항상 최신의 결과를 반환한다. 수정시에 클라이언트가 반드시 수행해야 하는 유일한 행위는 다시 표시하는 것인데, 그러면 renderer가 수정된 셀을 다시 랜더링하여 차례로 새로운 데이터 값을 조회하고 표시한다.

이러한 간접 접근의 한 결과는 통일된 클라이언트 데이터 캐쉬를 가질 수 있다는 것이다. 컴포넌트들이 중앙의 클라이언트 캐쉬로부터 iData 간접 접근과 DataObjects를 사용한다고 가정하면, 모든 데이터 수정 내용은 애플리케이션 전체에 동적으로 보관된다. 이것은 거래용 시스템과 동적 데이터를 표시해야 하는 여타의 클라이언트들을 상당히 단순화시킬 수 있다.

예: 가상 열 (virtual columns)
iData 기법의 유연성을 보여주는 한 예가 가상 열이다. 가상 열은 모델에서는 명시적으로 드러나지 않고 오히려 여러 필드들을 합성한 것이라 할 수 있는 데이터를 보관하는 열을 말한다. price와 cost간의 차이를 표현하는 profit이라는 열을 가정해보자. 그러한 열을 만들기 위해서는 getData()가 price와 cost 간의 차액을 반환하는 ImmutableIData 구현을 작성하면 된다.

Listing 4. 순익 가상 열


public class BicycleProfitImmutableIData extends DefaultImmutableIData
{
  ...
  
  public Object getData()
  {
    //return the difference of the price and cost field from the DataObject
    return new Double(bicycle.getPrice() - bicycle.getCost());
  }
}

표준 모델로 이러한 유형의 가상 행을 만들어 내려면 상당한 양의 로직이 필요할 것이다. 처음에는 모델이 정확한 값을 받을 것이다. 그러나 price 혹은 cost가 수정되었을 때 애플리케이션 전체에 profit 값을 수정하기 위해 복잡하고 에러 나기 쉬운 로직이 필요해지면서 문제가 발생한다. iData 기법에서는 price나 cost가 변하더라도 수정이 필요하지 않다. 영속성이 동적으로 이루어진다.

구성요소로 iData 객체 사용하기
간접 접근 층은 iData 객체의 그룹으로 구현되어 상당한 이점을 제공한다. 예를 들어, 두 가지 데이터 값을 더하는 PirceAndCost 표현은 또한 조합을 사용해서 구현될 수도 있다. 새로운 CompositePriceAndCost 표현에서 DataObject로부터 직접 양쪽의 데이터 값들을 조회하는 대신, 우리는 앞서 작성한 BicyclePriceImmutableIData와 BicycleCostImmutableIData 객체를 사용할 수 있다. getData() 메소드는 구분자 -이 경우에는 사선 (/)- 에 의해 표시된 두 iData 층 구현들로부터 조회된 값들을 첨가함으로써 반환 문자열을 만들어낸다. 결과로 나오는 getData() 메소드는 다음과 같다.

Listing 5. PriceAndCostImmutableIData의 조합 구현


public Object getData()
{
    // append the price, a slash, and the cost using pre-built iData 
    // implementations     
    return new String( (String)priceIData.getData() + " / " + 
        (String)costIData.getData() );  
}

서로 다른 iData 구현 내용을 결합하는 기술은 코드 재사용과 유연성을 향상시킨다. 여러분은 기존의 iData 구현들을 다르게 조합하여 새로운 iData 구현의 개발을 시작할 수 있다. 이러한 조합은 런타임시에 iData 구현 내용이 동적으로 조합되는 것을 용이하게 하기 때문에 유연성이 높아질 뿐 아니라 구체적인 클래스가 적어진다. 툴킷에는 간단한 조합 기반의 iData 객체를 구현하기 위한 다수의 헬퍼 클래스가 들어 있는데, 여기에는 iData의 문자열 표현을 장식하기 위해 접미사와 (혹은) 접두사를 가진 임의적인 iData 객체를 취하는 prefix and suffix string decorator iData 구현이 포함된다.

reflection 기반의 ImmutableIData 구현 (UniversalImmutableIData)
iData 방식의 최대 단점은 클래스 급증이다. iData 클래스의 수는 대단위 애플리케이션에서 빠르게 불어날 수 있다. iData 층 구현의 대다수는 같은 순서를 반복하는데, 여기에서 getData() 요청은 DataObject 내의 get[FieldName]() 메소드로 넘겨진다. 이것은 일반적으로 refection을 이용하여 구현된다. 툴킷에는 UniversalImmutableData라는 reflection 기반의 ImmutableIData 구현 내용의 기본 구현이 들어 있다. UniversalImmutableData는 초기의 매개변수로 한 DataObject와 필드 이름을 취한다. 내부적으로 UniversalImmutableData는 필드 이름으로 get[[FieldName]() 메소드를 조회하는데, 이 메소드는 getData()나 toString() 메소드가 불리워질 때 호출된다. 이런 방식은 reflection의 사용으로 인한 약간의 성능 저하를 가져오는 대신 개발을 단순화하고 클래스 급증을 줄인다. 대부분의 애플리케이션들은 이러한 성능 저하의 영향을 받지 않겠지만 큰 규모 혹은 실시간 애플리케이션 개발자들은 이 점을 염두에 두어야 한다.

Listing 6. UniversalImmutableIData를 사용하는 createRow() 메소드


protected Vector createRow(Bicycle bicycle)
{
  Vector vec = new Vector();
    vec.add(new UniversalImmutableIData(bicycle, "modelName"));
    vec.add(new UniversalImmutableIData(bicycle, "modelID"));
    vec.add(new UniversalImmutableIData(bicycle, "manufacturer"));
    vec.add(new UniversalImmutableIData(bicycle, "priceAndCost"));
    vec.add(new UniversalImmutableIData(bicycle, "inventory"));
  return vec;
}

Listing 7. reflection 기반의 UniversalImmutableIData


  protected String field = ...  //the field name 
  protected Method accessorMethod = ... //the accessor method
  protected Object source = ... //the DataObject

  ...

  protected void setMethods()
  {
    if (field == null || field.equals(""))
      return;

    //capitalize the first letter of the field, so you get getName, 
    //not getname...
    String firstChar = field.substring(0,1).toUpperCase();
    //remove first letter
    String restOfField = field.substring(1);
    //add together the string "get" + the capitalized first letter, 
    //plus the remaining
    String fieldAccessor = "get" + firstChar + restOfField;
    //cache the method object for future use
    this.setAccessorMethod(fieldAccessor);
  }


  ...


  protected void setAccessorMethod(String methodName)
  {
    try
    {
      accessorMethod = source.getClass().getMethod(methodName, null);
    }
    catch ( ... )
    {
      ...
    }
  }

  ...


  public Object getData()
  {
    try
    {
      return accessorMethod.invoke(source, null);    
    }
    catch ( ... )
    {
      ...
    }
  }

편집 가능한 지능형 데이터(MutableIData)를 위한 데이터 간접 접근 층
MutableIData는 ImmutableIData를 변경 가능하도록 만들기 위해 setData() 메소드를 추가해 ImmutableIData를 확장한다. s
etData() 메소드는 새로운 데이터 값을 매개변수로 취하며 변경이 성공했는지를 보여주는 부울 값을 반환한다. DataObject의 해당 필드에 대해 캐쉬된 데이터 유형과 일치시키기 위해서는 보통 새로운 데이터 값을 setData() 메소드에 전달해야 한다. 표준 구현은 객체 유형을 안전하게 테스트하여, 클래스 유형이 일치하지 않는 경우 false를 반환한다.

Listing 8. 자전거 제조 업체를 위한 MutableIData


  public boolean setData(Object data)
  {
     if (!data instanceof String) 
         return false;
     ((Bicycle)this.getSource()).setManufacturer((String)data);
         return true;
  }

일단 모든 새로운 MutableIData 객체가 작성되면, 대응되는 Immutable 인스턴스 대신에 MutableIData 인스턴스를 인스턴스화하기 위해 getRow() 메소드를 수정한다. 나는 한 필드가 변경 불가능할 경우 단지 ImmutableIData 객체를 만들면 된다는 것을 발견하였다. 그 외의 방법으로 나는 setData() 메소드가 호출되지 않을 것임을 아는 상태로 MutableIData를 만들고 그것을 간단히 읽기 전용 테이블에서 사용한다.

사용자 정의 편집기들
여러분의 데이터를 수정 가능하게 만들었기 때문에 커다란 변화가 존재한다: 즉 데이터를 편집하려면 사용자 정의 편집기가 필요하다는 것이다. 기본 편집기를 사용할 경우 JTable은 Object 유형의 데이터 편집기를 부를 것인데, 이는 실제로는 String 편집기이다. 일단 편집이 끝나면 편집기는 모델에 보관될
String 값을 반환하는데, iData 층의 구현을 String 값으로 대체한다. 다음의 도표는 편집기가 iData 간접 접근의 무결성을 유지하기 위해 따라야 하는 순서를 나타낸 것이다.

그림 5. 편집 순서
Editing sequence

비록 기존의 편집기들이 이러한 순서를 따르도록 확장될 수 있지만 그러한 방식은 비현실적이다; 이것은 클래스 급증으로 이어지며 불필요한 복잡성을 야기한다. 사용자 정의 편집기들이 독자적인 상황에서 실용적이지만 대부분의 편집기들이 같은 순서를 따를 것이며, 이는 개별적인 클래스에 캡슐화 것이다. iData 툴킷은 UniversalTableCellEditor라고 불리는 이 클래스의 구현을 담고 있다.

UniversalTableCellEditor는 확장이 아닌 TableCellEditor 보관 방법을 사용한다. 편집시에 UniversalTableCellEditor는 iData 층의 구현으로부터 데이터 값을 가져와 그 값으로 포함하고 있는 TableCellEditor를 초기화한다. 편집이 끝나면 UniversalTableCellEditor는 TableCellEditor로부터 값을 조회하고 그에 따라 iData 구현에 데이터를 설정한다. 만일처음에 아무 편집기가 지정되지 않으면, UniversalTableCellEditor는 iData 구현의 데이터 유형에 대해 JTable내의 기본 편집기를 불러 온다.

위에서 설명한 모든 편집 순서는 UniversalTableCellEditor내에 완전히 캡슐화된다. 이것은 iData 로직을 구현하기 위해 third-party 편집기를 포함한 어떤 편집기도 확장될 필요 없이 사용될 수 있음을 의미한다.

나는 각 TableColumn 기본 편집기를 UniversalTableCellEditor에 설정함으로써 JTable 내에 편집기를 설정하기를 권한다. iData 툴킷은 다수의 정적인 헬퍼 메소드를 가지고 있는 유틸러티 클래스를 포함하고 있다. 유틸러티 클래스 내의 configureTable() 메소드는 TableColumns을 통해 반복되어, 그 컬럼에 대한 이전의 셀 편집기를 가지고 있는 UniversalTableCellEditor의 새로운 인스턴스에 각각의 현재 편집기를 설정한다.

툴킷에는 renderer와 유사한 기능을 가진 UniversalTableCellRenderer가 들어 있다. JTree와 JComobBox/JList를 위한 범용 편집기와 renderer 조합들도 툴킷에 들어 있다.

예: 가격이 비용보다 높음을 보장하기 위한 셀 내 검증 작업
편집의 일반적인 어려움은 셀 편집이 끝나기 전에 데이터가 검증되어야 하는 in-cell validation (셀 내 검증 작업)에 있다. setData() 메소드는 셀 내 검증 작업을 위해 중앙 집중화된 저장소를 생성한다. 어느 한 값이 편집된 후 가격이 비용보다 적다면 사용자에게 통지해야 하는 예를 가정해보자. 이 예에서 우리는 사용자에게 다음의 옵션이 제시되길 원한다.

  • 두 값 모두 원래의 값들로 놔두기
  • 가격을 비용과 동일하게 올리기
  • 편집이 안된 값을 변경하여 처음의 값과 이제 막 편집된 값 간의 원래 가격 차이를 유지하도록 하기

이 모두는 setData() 메소드에서 비교적 쉽게 이루어진다. JOptionPane은 사용자가 선호하는 옵션을 확인하도록 하기 위해 표시된다. 일단 옵션이 선택되면 계산이 수행되어 적절한 값을 적용한다. 이러한 비즈니스 로직을 구현하기 위한 중앙집중화된 저장소뿐 아니라 모든 데이터 값들에 대한 정보는 iData 기법의 유연성에 핵심이 된다.

Listing 9. 셀 내 검증 작업


String doNotEdit = "Do Not Edit";
String priceEqualsCost = "Price = Cost";
String keepProfitDifference = "Keep Profit Difference";
String keepProfitPercentage = "Keep Profit Percentage";

...

public boolean setData(Object data)
{
    double newCost = new Double(data.toString()).doubleValue();
    double oldCost = this.bicycle.getCost();
    double price = bicycle.getPrice();

    ((Bicycle)this.getSource()).setCost(newCost);

    if (price < newCost)
    {
      Object result = JOptionPane.showInputDialog
      (
        null,
        "Cost you have entered is more than the set price for this bicycle"
        + "\nPlease select from the following options",
        "",
        JOptionPane.QUESTION_MESSAGE,
        null,
        new Object[]{doNotEdit, priceEqualsCost, keepProfitDifference, 
            keepProfitPercentage},
        priceEqualsCost
      );

      if (result != null)
      {
        //persist the data
        if (result.equals(priceEqualsCost))
          this.bicycle.setPrice(bicycle.getCost());  
        //keep the delta between price and cost
        else if (result.equals(keepProfitDifference))
          this.bicycle.setPrice( newCost + (oldPrice - oldCost) ); 
        //keep the same profit percentage
        else if (result.equals(keepProfitPercentage))
          this.bicycle.setPrice( newCost * (oldPrice / oldCost) ); 
      }
    }
    return true;
  }

비 JTable계열의 컴포넌트 사용하기
일관성을 위해 우리의 예에서 JTable을 사용해 왔지만, 또 다른 Swing 컴포넌트를 이용하는 간단한 예를 살펴보는 것도 의미가 있을 것이다. 자전거 이름을 담고 있는 JList를 만들어보자. 간단히 Bicycle 객체들의 집합을 계속 반복하고 이들을 BicycleModelNameImmutableData 객체 내에 넣은 후 JList에 추가한다. JTable에서 사용되었던 똑같은 iData 인스턴스들이 Jlist에서도 사용됨에 주목한다. 이러한 인스턴스들은 같은 방식으로 어떤 다른 컴포넌트에서도 사용될 수 있다.

Listing 10. JList 초기화


protected void initList()
{
    ...

    while ( ... )
    {
       //wrap the bicycle in an iData object and add it to the list model
      Bicycle bicycle =  ...
      model.addElement(new BicycleModelNameMutableIData(bicycle)); 
    }
    //wrap the lists renderer in the iData toolkit universal renderer 
    //for JLists
    list.setCellRenderer(new 
      UniversalListCellRenderer(list.getCellRenderer())); 
}

그림 6. JList의 예
JList example

지능형 화면 간접 접근 (iDisplay)
iDisplay 구조는 iData에 기반하여 사용자 정의 화면을 생성하는 층이다. 예를 들어, 사용자에게 선호되는 Manufacturer를 알려주기 위해 그 Manufacturer가 빨간 문자로 표시되어야 한다고 가정하자. 이는 보통 renderer에 의해 전달되지 않는 데이터 값들을 조회해야 하는 복잡한 로직을 필요로 하며, 그 결과 데이터 중심의 사용자 정의 화면을 비현실적으로 만들어 버리는 복잡한 코드를 낳을 것이다. iDisplay는 그러한 시나리오를 단순하게 만들기 위해 iData와 밀접하게 통합하며, 그 과정에서 또한 확장을 위한 중앙 저장소를 생성한다.

읽기 전용의 지능형 데이터를 위한 화면 간접 접근 층 (ImmutableIDIsplay)
ImmutableIDisplay는 화면에 따라 독자적인 로직을 캡슐화한다. 이것은 다음과 같은 세가지 주요 renderer 유형에 대해 get[Component]CellRendererComponent() 메소드를 가지는 ImmutableIDisplay에 의해 수행된다: TableCellRenderer, TreeCellRenderer 및 ListCellRenderer가 그것이다. ImmutableIDisplayIData는 ImmutableIDisplay를 포함시킴으로서 ImmutableIDisplay를 ImmutableIData와 통합한다.

JTable이 UniversalTableCellRenderer내의 getCellRendererComponent()를 호출하고 ImmutableIDisplayIData 유형의 객체에 이를 전달할 때, UniversalTableCellRenderer는 getCellRendererComponent 요청을 ImmutableIDisplayIData가 보유하고 있는 ImmutableIDisplay내의 상응하는 get[Component]CellRendererComponent에게 전달한다.

우리의 자전거 상점 데모로부터 또 다른 예를 살펴보자. 이 예에서 사용자가 자전거의 price를 입력하는데 이 값이 cost보다 적어서 price와 cost 셀의 바탕색이 빨간색으로 변하게 된다. ImmutableIDisplay의 getTableCellRenderer() 메소드는 DataObject를 호출하고 price가 cost보다 적은지를 확인한다. 만약 그렇다면 바탕색은 빨간색으로 설정된다; 그 외의 경우는 흰색으로 설정된다. 특수한 경우가 제시되지 않은 경우에는 배경을 분명히 기본 색으로 설정할 것을 기억하는 것이 중요하다. Swing은 랜더링에 있어 경량화된 패턴을 사용하는데, 같은 컴포넌트를 되풀이해 색칠한다. 만일 표준 설정이 특수한 경우로 인해 바뀌었는데 표준의 경우로 재설정되지 않았다면 예상치 못한 결과가 벌어진다.

Listing 11. 데이터 기반의 셀 색칠을 보여주는 자전거 비용에 대한 getTableCellRenderer() 메소드


public TableCellRenderer getTableCellRenderer(JTable table, Object value, 
  boolean isSelected, boolean hasFocus, int row, int column)
{
      //cache old background for change comparisons
      Color oldColor = renderer.getBackground(); 
      //cache old background for change comparisons
      Color newColor = null;
      //check to see if Object is a MutableIData
      if (value instanceof MutableIData)
      {
        MutableIData arg = (MutableIData)value;  //cast it.
        Bicycle bicycle = (Bicycle)arg.getSource();
        if (arg.getData() instanceof Number)  //check the data type
        {
        // retrieve price and cost from the DataObject
          double cost = ((Number)arg.getData()).doubleValue();
          double price = bike.getPrice();
          //make comparisons
          if (price > cost)
            newColor = Color.cyan;
          else
            newColor = Color.red;
        }
      }
      // check and see if color changed
      if (!newColor.equals(oldColor))
          this.setBackground(newColor);
}

그림 7. 가격과 비용에 대한 배경색 검증 테이블
Table with price and cost background color validation

편집 가능한 지능형 데이터에 대한 화면 간접 접근 층 (MutableIDisplay)
변경 불가능/변경 가능의 구분은 iDisplay 구현에서도 마찬가지이다. MutableIDisplay는 편집기에 해당하며, ImmutableIDisplay는 renderer에 해당한다. ImmutableIDisplayIData에서와 마찬가지로 MutableIDisplayIData는 MutableIData를 확장하며 MutableIDisplay를 포함하고 있다. 이것의 사용 방법은 get[Component]CellRenderer() 메소드가 아닌 get[Component]CellEditor() 메소드를 구현한다는 점을 제외하면 ImmutableIDisplay의 사용법과 동일하다. 툴킷에는 JTable, JTree 및 JComboBox용 사용자 정의 편집기가 들어 있다.

get[Component]CellRenderer()와 get[Component]CellEditor() 메소드를 iDisplay에 전달하면 유용한 간접 접근 층이 생성된다. 주요한 결과로는 화면 설정과 기능을 커스터마이징하기 위한 중앙의, 캡슐화된 저장소가 만들어진다는 것이다. 확장 대신에 iData를 iDisplay에 대한 containment로 사용하면 클래스 급증을 제한하는 것 외에 유연성과 확장성을 증진시킨다. 가장 중요한 것은 종종 매우 복잡한 화면 로직을 담고 있는 사용자 정의 편집기와 renderer의 필요성을 거의 없앤다는 점이다. 비록 완전한 형태의 사용자 정의 편집기와 renderer가 필요하긴 하지만 화면의 대다수는 iDisplay가 제공하는 간접 접근 층을 사용하여 구현될 수 있다.

단점
iData 기술을 구현할 때 염두에 두어야 할 몇 가지 단점이 있다.

  • 성능 : iData 기법은 대부분의 애플리케이션에 있어 상당한 성능상의 부하를 가져 오지 않는다. 이 기법은 로직이나 프로세싱이 아닌 상당한 양의 간접 접근을 정의하기 때문이다. 그렇지만 만약 getData()/setData() 메소드 혹은 get[콤포넌트]CellRenderer()/Editor() 메소드들의 구현 내용이 너무 많은 로직을 갖고 있다면 문제가 발생할 것이다. 컴포넌트가 색칠될 때마다 컴포넌트 내의 모든 셀에 대해 이 메소드 내의 어떤 로직이 호출된다. 이 메소드를 가능한 한 간결하게 유지하도록 한다.
  • 코드베이스에 추가되는 클래스들 : iData 기술을 사용하려면 수많은 클래스가 필요하다는 사실은 의심의 여지가 없다. 이것은 어떤 객체 지향 기법에서도 예상되는 것이고, 또한 일정 부분 이점도 있다. 사실 특정 애플리케이션에 국한된 비즈니스 로직의 대다수가 이러한 가외의 클래스에 들어 있고, 이는 그렇지 않았다면 발생하지 않았을 캡슐화가 이루어지도록 한다. 만일 클래스들이 최소로 유지되어야 한다면 이것은 최선이 선택이 아닐 수 있다. 보통 성능 비용의 문제가 있긴 하지만, 이렇게 규모에 민감한 애플리케이션을 위해 많은 최적화 방법이 소개되고 있다. 코드의 복잡성, 클래스의 수, 그리고 성능 비용에 대한 결정을 내릴 때 애플리케이션의 요구사항이 고려되어야 한다.
  • 학습 곡선 : 이것이 가장 두드러진 단점이다. iData 기법은 유연성과 확장성을 염두에 두고 설계되었다. 이것은 처음에는 기를 죽이거나 혼란스럽게 하는 상당한 양의 추상화를 필요로 한다. 나는 어느 정도의 연구 후에 이 아키텍처에 접근할 수 있다고 믿지만, 꾸준한 주의가 필요하다.

결론
컴포넌트 모델을 iData 간접 접근 층과 결합된 지능형 데이터로 채우면 고급 UI 기능을 구현하기 위한 유연하고 확장성 있는 중앙의 저장소를 만들 수 있다. 또한
이 기능은 유연성과 재사용성을 증진시키기 위해 완전히 캡슐화된 클래스들 내에 비교적 단순한 로직으로 구현될 수 있다. 여기에 오픈 소스 툴킷이 더해짐으로써 iData 기법의 통합이 쉬워진다. 많은 코드가 이미 작성되고 테스트되어 있기 때문이다. 설명된 기법을 성공적으로 사용하기 위해 각 애플리케이션이 필요로 하는 것은 iData 간접 접근 층의 자체 구현 뿐이다. 여기에는 Swing 컴포넌트에 대한 중대한 커스터마이제이션도, 사용자 정의 모델도, 그리고 표준 Swing 기능의 변경도 없다. 단지 주의 깊게 배치한 간접 접근이 있을 뿐이다. 결과적으로 복잡한 화면 기능의 구현과 커스터마이제이션을 직관적이고, 유연하며, 확장 가능한 방식으로 단순화시킨 시스템이 나오는 것이다.

오픈 소스 note
나는 iData 방식을 성공적으로 구현하는데 툴킷이 필수적이라고 믿고 있다. 툴킷은 여러 번 개발된 최적화 및 헬퍼 클래스뿐 아니라 사용자 테스트를 거친 코드를 제공하여 통합 시간을 최소화시켜 준다. 설계 및 구현은 내가 참여한 프로젝트의 영향을 받았다. 분명히 여러분은 툴킷을 수정하면 훨씬 쉽게 구현될 수 있을 요구 사항을 가지고 프로젝트를 찾아냈을 것이고, 개발자들이 그 설계를 개선시키는 것을 도와 보다 접근성 있고 강력한 구현을 만들어내기 때문에 그 유용성은 훨씬 더 커질 것이다. 고

툴킷은 Artistic 라이선스 하에 SourceForge.net을 통해 배포된다. 프로젝트 홈페이지 (참고자료)에는 전체 소스, 문서 및 바이너리 배포판 뿐 아니라 listserv와 기타 정보에 대한 링크도 있다. 여러분은 향후 출시판에 포함시킬 코드 개선에 기여해 달라는 격려를 받는다. 오픈 소스 협약의 규정에 따라 모든 애플리케이션에 대한 이용이 무료이다.

참고자료

목 차:
iData 기법의 세 가지 층
DataObjects
간접 접근 : iData 층
지능형 화면 간접 접근 (iDisplay)
단점
결론
오픈 소스 note
참고 자료
필자 소개
기사에 대한 평가
관련 dW 링크:
오픈 소스 note
JavaBeans: The Perfect Roast?
자바 빈 Reflecting, introspecting 및 커스터마이징하기
Swing의 JTable 컴포넌트에서 셀 렌더링하기
자바 빈 Reflecting, introspecting 및 커스터마이징하기
Subscribe to the developerWorks newsletter
US 원문 읽기
Also in the Java zone:
Tutorials
Tools and products
Code and components
Articles
필자소개
Jonathan Simon은 현재 JPMorganChase의 채권 거래 시스템의 클라이언트 개발을 책임지고 있다. 그는 Swing, 실시간 클라이언트, third-party 컴포넌트 suite, 클라이언트 아키텍처 및 클라이언트 배치등과 같이 클라이언트 측 기술의 여러 면에 경험을 가지고 있다. Jonathan은 또한 음악 학교에서 사사한 타악기 연주자로서, 음악 연주와 음악 및 음성 관련 소프트웨어 개발을 즐긴다.
이 기사에 대하여 어떻게 생각하십니까?

정말 좋다 (5) 좋다 (4) 그저그렇다 (3) 수정보완이 필요하다(2) 형편없다 (1)

  회사소개  |  개인정보 보호정책  |  법률  |  문의