여전히 많은 이클립스 개발자들은 SWT클래스를 상속받습니다. 이 관행은 사실 Swing으로부터 전해져 온 것인데, Swing이 실패한 주요원인 중하나 입니다. 구체적 내용에 대하여는 차차 언급합니다. 이 글을 읽기 전에 먼저 SWT가 상속을 불허하는 이유를 읽어 보시기 바랍니다.

UI 재사용성 허구

여러분이 만든 UI, 예를 들어 검색 폼이나 입력 폼. 그 UI가 실제로 재사용된 적이 있나요? 혹은 그 UI가 다른 모델이나 컨트롤러와도 함께 일할 수 있나요? 그렇다고 대답하실 수 있는 분은 상당히 드물겁니다. 그렇다고 대답된 UI 들은 대체로, 전용성과 풍부한 경험이 떨어질 것 또한 분명하죠. 재사용의 단위는 위짓이나 컨트롤일 뿐, 전체 화면 설계가 아닙니다.

결국 화면 설계 클래스를 정의하는 모듈화는 의미가 없습니다. UI의 상속은 결코 일어나지 않을 일들에 대한 가치를 중시하는 행동 중 하나입니다. UI는 목적에 따라 그 특성이 매우 다르기 때문에, 한 UI가 상속받아 필요한 부분만 추가하거나 변경한 경우, 부모와 자식이 모두 가치가 있을 확률이 거의 없습니다.

상속 그 자체의 문제점

상속은 오랫동안 유용한 OOP도구로 인정 받아왔습니다, 하지만 세상이 점 점 더 복잡해짐에 따라 여러가지 문제가 발생합니다.

첫 째, 상속은 부모 클래스에 대하여, 너무 많은 지식과 이해를 필요로 합니다. 이는 객체지향에 위배됩니다. 객체지향에서 한 영역내에서 다른 객체는 역할 수행자로 취급되며, 내부는 철저히 블랙박스로 취급되어야 합니다. 그러나 상속을 받기 위해서는 부모의 모든 것을 알고 있어야 합니다. 언제 protected 메서드가 호출되는지, 내부 필드나 상태가 어떤 경우에 어떤 시나리오로 업데이트 되는지, 어떤 메소드를 오버라이드 해도 되는지, 어떤 경우 반드시 슈퍼 메소드를 호출해야 하는지, 그리고 이 과정은 모든 부모 체계를 따라가며 필요해 집니다. 이것인 일반적으로 객체가 협력하는 패턴에 위배되며, 객체성에 손실을 가져옵니다.

둘 째, 부모가 변경될 때 자식은 그 변화를 안전하게 감당할 수 없습니다. 여기에 관해서 *연약한 부모 문제*를 읽어 보세요.

간단히 말해서, 이 일련의 과정이 안전하게 수행되는 것은 불가능 합니다. 다시 한 번 강조합니다. 확장이 의도된 클래스와 자세한 방법이 기술된 문서가 있지 않은이상, 상속을 통해 정확히 자기가 원하는 부분만 부작용 없이 확장하는 것은 *불가능*합니다. 단지 그것은 운에 의지하는 해킹일 뿐입니다.

컴포지트 패턴

그러나 우리는 우리가 필요로 하는 것들을 골라서 조립해서 사용할 경우, 우리는 각 컴포넌트에 대해 우리가 필요로 하는 것만 이해하면 됩니다. 각 컴포넌트는 버전업이 되더라도 퍼블릭 인터페이스에 기술된 약속을 항상 지켜줄 것입니다. 조립에 의해 나타날 수 있는 패턴은 무한대이며, 필요에 따라 다양하기 때문에, 이에 대한 모듈화는 무의미합니다.

필요한 UI를 조립한다는 것은 UI를 직접 정의하는 것과는 전혀 다른 아이디어 입니다. 이미 존재하는 UI들을 조립하는 것은 뷰가 아니라 컨트롤러에 가깝습니다. JFace는 바로 이러한 패턴에 아주 충실합니다. 쓸만하고 강력한 트리를 만들기 위해서 TreeViewer는 SWT의 Tree를 상속받지 않습니다. 다만 이를 이용할 뿐입니다. 마찬가지로 JFace의 많은 Dialog들 역시, SWT 의 Shell을 상속받지 않습니다. 마찬가지로 이를 이용할 뿐입니다.

기억하세요, UI를 상속받아 특화 하지말고, 조립만 하십시오.

그런데 많은 경우, 컴포넌트를 단지 조립하기 위해서 Shell이나 Composite를 상속받는 실수를 범하곤 합니다. 이 경우에는 다음과 같은 문제가 있을 수 있습니다.

상속을 통한 컴포넌트 조립의 문제점

예를 들어 Person 객체에 대해 이름과 나이를 표시하는 두개의 Label을 갖는 Composite를 상속받는 PersonView라는 클래스를 만들었다고 해 봅시다.

코드:

public class PersonView extends Composite {
	private Text nameText;
	private Text ageText;

	/**
	 * Create the composite.
	 * @param parent
	 * @param style
	 */
	public PersonView(Composite parent, int style) {
		super(parent, style);
		setLayout(new GridLayout(2, false));
		
		Label nameLabel = new Label(this, SWT.NONE);
		nameLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
		nameLabel.setText("이름");
		
		nameText = new Text(this, SWT.BORDER);
		nameText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
		
		Label ageLabel = new Label(this, SWT.NONE);
		ageLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
		ageLabel.setText("나이");
		
		ageText = new Text(this, SWT.BORDER);
		ageText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
	}

	@Override
	protected void checkSubclass() {
		// SWT의 상속 불허 제약을 방지.
	}
}

이 클래스는 다음과 같은 문제가 있습니다:

  • PersonView는 너무나 많은 public API를 갖게 될 것입니다. 왜냐하면 Composite가 가진 public API 가 그대로 노출 될 것이기 때문이죠. 그러나 대부분의 개발자가 갖는 주요 관심사는 setInput(Person person)이라는 메서드 한 개 일 것입니다. 결국 여러분은 너무 쓰기 어려운 콤포넌트를 공급한 셈입니다.
  • 여러분은 Person 객체의 name이나 age값이 변경될 때 자동으로 업데이트 되면 좋겠다고 생각할 것입니다. 그러나 이를 구현하는 순간 이로 인해 여러분의 UI는 모델과의 응집성을 피할 수 없게 됩니다. 이 클래스 자체가 UI 이기 때문입니다. 그렇다고 이러한 기능을 외부에 작성하는 것도 애매모호해 집니다. 이 클래스의 주요임무가 Person객체를 보여주는 것인데 그것이 외부에 작성된다면 롤자체가 불분명해지기 때문입니다. 결국 MVC가 뒤죽박죽인 클래스가 나올 수 밖에 없습니다.

네이티브별 상이한 구현 체계

다음은 Cocoa SWT의 Control#getSize()의 내부입니다:

public Point getSize () {
	checkWidget();
	NSRect rect = topView().frame();
	return new Point((int)rect.width, (int)rect.height);
}

일반적인 JNI가 동일한 자바 클래스에 시스템 라이브러리만 바뀌는 것과 달리 SWT는 아예 그 클래스 자체가 달라지며, 다이나믹 네이밍 룩업으로 연결됩니다. NSRect라는 클래스가 보이는데 이는 오로지 맥용 SWT에만 존재합니다. 따라서, 여러분이 SWT의 동작을 수정할 때 protected 메서드등을 사용하거나 동작을 변경하는 것은 매우 위험한 행동입니다.

가능한한 그러한 작업을 수행하지 마십시오. 꼭 해야 한다면, 퍼블릭 클래스 스키마에 포함되는 메서드만 사용하십시오.

JFace 스타일 일반 원리

이제 우아함에 대해 좀 이야기 해 봅시다.

JFace -> SWT UI: 생성/조립
사용자 -> SWT UI: 상호작용
SWT UI -> JFace: 이벤트 전달
JFace->JFace: 컨트롤링
JFace->SWT UI: UI 업데이트
SWT UI -> JFace: 디스포즈 이벤트
JFace -> JFace: 사용한 자원들 정리

JFace에서는 필요한 위짓들을 직접 조립합니다. 대게는 생성자나 create(Composite parent)메서드를 통해 그러한 일을 수행합니다. 그리고 이러한 UI명세는 별개의 클래스로 모듈화 하지 않습니다 단지 create과정중에 조립될 뿐입니다.

일단 UI가 만들어지고 나면 JFace객체는 컨트롤링에만 집중하며, 그 과정중에 몇몇 헬퍼 객체의 도움을 받을 수도 있습니다. 예를 들어 LabelProvider나 ContentProvider등이 그렇습니다.

이러한 방식의 장점은 다음과 같습니다:

  • JFace 객체는 정확히 사용자가 관심을 가지는 메서드만을 나열 할 수 있다. (컨트롤러 작성을 쉽게 함)
  • JFace 객체는 모델의 언어와 뷰의 언어를 중간에서 번역하는 역할을 수행함으로써 모델과 뷰를 분리 시켜준다. 사용자는 SWT에 대해 몰라도 되며, 자신의 모델과 비즈니스 로직에만 충실하면 된다.
  • UI의 생명주기로 부터 JFace객체는 자유롭다.

UI의 배치나 레이아웃 데이터 지정등을 위해 대부분의 JFace객체는 getControl()을 제공하며 이를 통해 UI 구성작업 따로 분리 시킬 수 있습니다.

앞서의 예제를 JFace스타일로 작성하면 다음과 같습니다:

public class PersonView {
	private Composite composite;
	private Text ageText;
	private Text nameText;

	/**
	 * Create the composite.
	 * 
	 * @param parent
	 * @param style
	 * @wbp.parser.entryPoint
	 */
	public void create(Composite parent, int style) {
		this.composite = new Composite(parent, style);
		this.composite.setLayout(new GridLayout(2, false));

		Label nameLabel = new Label(this.composite, SWT.NONE);
		nameLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
		nameLabel.setText("이름");

		this.nameText = new Text(this.composite, SWT.BORDER);
		this.nameText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));

		Label ageLabel = new Label(this.composite, SWT.NONE);
		ageLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
		ageLabel.setText("나이");

		this.ageText = new Text(this.composite, SWT.BORDER);
		this.ageText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));

		this.composite.addDisposeListener(new DisposeListener() {
			@Override
			public void widgetDisposed(DisposeEvent e) {
				dispose();
			}
		});
	}

	protected void dispose() {

	}

	public Control getControl() {
		return this.composite;
	}
}

이 클래스는 부모 클래스가 없기 때문에 깔끔한 API의 공급이 가능하며, UI관련 처리는 getControl() 을 통해 가능합니다.

create메서드에 @wbp.parser.entryPoint 라는 어노테이션이 달려있는데, 이는 윈도우즈 빌더 프로가 이곳이 UI 를 조립하는 지점임을 알게 해 줍니다.

그렇다면 SWT의 상속은 전혀 불필요한가?

그렇지는 않습니다. SWT가 반대하는 것은 이미 의도된 컨트롤의 동작을 변경하거나, 단순 조립 목적으로 상속하는 것입니다. 그러나 전혀 새로운 종류의 위짓이 필요하다면 Canvas 를 상속받아 만들어야 합니다. 그러나 이 경우, 기술적으로 비록 상속이라고 하더라도, 개념상으로는 새로운 종류의 위짓을 만드는 것입니다.

SWT 상속이 필요한 경우의 예

Posted by 지이이이율
,

컬럼 위치를 드래그로 변경

TreeViewerColum viewerColumn = ...
viewerColumn.getColumn().setMovalble(true);

의외로 많은 사람들이 이 기능을 직접 구현하기 위해 삽을 듭니다. 또한 setColumnOrder()와 getColumnOrder()로 상태를 보존 할 수 있습니다.

컬럼에 정렬 아이콘 표시하기

TreeViewer viewer = ...;
viewer.getTree().setSortColumn(column);
viewer.getTree().setSortDirection(isAscending ? SWT.DOWN : SWT.UP);

이 역시, 많은 사람들이 column의 setIcon()을 통햐 직접 지정하는 실수를 많이 저지르고 있습니다. 이 두 메서드는 소팅 컬럼의 아이콘과는 별개로 위쪽 또는 아래방향 화살표의 정렬 아이콘을 추가적으로 표시할 뿐, 실제 정렬기능을 수행하지는 않습니다. 실제 정렬기능은 setSorter(ViewerSorter)를 이용하세요. 진입점은 column 위젯의 selection 이벤트 입니다.

Posted by 지이이이율
,

이 글은 SWT, the standard widget toolkit의 챕터1 섹션2의 내용을 일부 발췌하여, 이해하기 쉽게끔, 의미상으로 번역되고 편집되었습니다.

연약한 부모 문제

객체지향 언어에서 상속은, 소위 *연약한 부모 문제*라는 문제 때문에, 완벽하게 안전하다고 볼 수 없습니다.

*연약한 부모*라는 말은 C++세계에서 유래 했는데, 보통은 부모 클래스에 새로운 메서드나 필드를 추가하면, 그를 원래부터 상속받던 자식 클래스들이 모두 재 컴파일하기 전까지 작동하지 않거나, 메모리 할당량이 부정확해지던 문제를 지칭합니다.

자바의 경우 부모 클래스가 바뀌더라도, 네임 룩업 매커니즘이 알아서 정적인 클래스 호환문제를 자동으로 해결합니다. 심지어 개발자는 그 존재조차 알 필요가 없습니다. 그러나 연약한 부모 문제는 다이나믹한 영역도 가지고 있습니다.

예를 들어 자식 클래스는 부모 클래스가 내부 구현상으로 호출해줄 public 메서드를 오버라이드하여 확장되었을 수 있습니다. 이경우 정적인 클래스 스키마 뿐 만 아니라, 그 메서드를 호출 해 줄 것이라는 *사실*에 근거하여 확장이 이루어 진 것입니다. 그런데 부모 클래스의 구현이 변경되어 더이상 그 메서드를 호출해 주지 않는다면, 정적인 컴파일에는 문제가 없지만, 오작동하게 될 것입니다. 이런 경우 문제를 찾아내는 것은 매우 고된일이 될 것입니다.

SWT는 플랫폼 별 내부구현이 매우 다르고, 변화의 속도가 빠르기 때문에, 부모 클래스가 어떻게 작동한다 하는 *사실*에 근거하여 상속했다가는 큰 낭패를 볼 수 있습니다. 이러한 이유로 SWT의 위젯 클래스들은 임의 상속을 허용하지 않습니다.

강제 임시 상속

보통은 금지 되어있는상속을 꼭 받아야 하겠다면, 위젯의 checkSubclass()를 오버라이드 해야 합니다. 이 메서드는 기본적으로 허가된 확장이 아닐 경우 SWTException을 던지며, 생성된 직후에 호출 됩니다. 아래는 Label을 상속받아 setText를 오버라이드 한 예입니다:

Label label = new Label(shell, SWT.NONE) {
    protected void checkSubclass() {
        // 아무일도 안하게 오버라이드 함.
    }

    public void setText(String string) {
        System.out.println("Setting the string");
        super.setText(string);
    }
};

자바는 class 앞에 final이라는 키워드를 이용하여 간단하게 상속이 불가능하다고 태깅할 수 있습니다. 사실 아주 예전의 SWT는 이 키워드를 사용했습니다. 불행하게도, 이 방법은 지나치게 유연성이 부족함이 증명되었습니다. 특정한 경우 SWT의 클래스에서 에러가 발생되더라도, 개발자들이 이를 우회할 방법이 없었던 것입니다. 개발자는 임시 상속을 통한 패치나, 버전 호환성을 위한 트릭을 사용할 수 없이, 하염없이 SWT팀이 문제를 수정하고 릴리즈 할 때까지 기다려야 했습니다.

이러한 이유로 SWT 위젯들은, 다소 이상해 보이는 상속 제약이 있지만, 이러한 뒷 배경이 있습니다.

Posted by 지이이이율
,

이 문서는 큰 스케일에서도 트리나 테이블로 만들어진 뷰가 성능을 유지할 수 있는 방법을 설명합니다.

필요 선수지식

  • SWT Table 또는 Tree
  • JFace TableViewer 또는 TreeViewer
  • 글쓴이가 왠지 잘 생긴 사람일 것 같은 예감

기존 트리/테이블 뷰어의 문제점

JFace 뷰어 에서는 특정 모델에 대한 UI(트리아이템이나 테이블 아이템)를 업데이트 하기 위해서, 개발자가 UI와 모델의 관계를 일일히 고민하지 않아도 된다. 특정 모델과 연결된 UI를 갱신하기 위해서는 단지 아래와 같은 코드를 이용하면 된다:

// myModel 에 해당하는 테이블 로우(TableItem)을 갱신한다.
tableViewer.update(myModel, null);

개발자가 UI를 신경쓰지 않고 모델 레벨에서만, 집중력을 유지할 수 있도록 하는 이 우아한 방식의 문제점은, 모델의 갯수만큼 뷰(TableItem/TreeItem)을 만든다는 점이다. 모델이 아주 많은 구성요소를 갖거나, 그 구조의 변경이 많이 일어나는 경우, UI 비용도 그만큼 높아져 필연적으로 성능문제가 나타난다.

모델의 변경이 복잡하게 일어나더라도 모델은 대게 경량 객체이기 때문에 문제가 되지 않지만, UI갱신은 GDI자원을 이용할 뿐만 아니라, UI스레드에서만 수행되기 때문에, 조금만 과부하가 걸려도 사용자는 성능저하를 겪게 된다.

SWT.VIRTUAL

이클립스 3.2 부터, 트리와 테이블은 SWT.VIRTUAL 이라는 플래그를 새롭게 지원하기 시작했다.

SWT 레벨에서 VIRTUAL 플래그는 물리적으로 단순히 트리 아이템이나 테이블 아이템이 Widget.setData(Object) 메서드를 통해 data를 소유할 수 있도록 허가하는 역할 밖에 하지 않는다.

하지만, 이를 허가함으로 인하여 각각의 트리 아이템이나, 테이블 아이템들은 입력 모델을 가질 수 있게 되어, 결과적으로 마이크로 뷰어 역할을 수행할 수 있는 기반을 갖게 된다. 예를 들어, 아이템을 그대로 둔 체 data만 교체한다면, 테이블 아이템이나 트리 아이템은 완벽하게 스스로 MVC구조를 갖춘 재사용가능한 뷰어 역할을 할 수 있을 것이다.

JFace에서의 VIRTUAL 플래그

테이블 뷰어나 트리뷰어는 SWT.VIRTUAL 플래그가 있는 경우, 화면에 *보이는 만큼만* 아이템을 만들고, 스크롤이 되거나, 모델이 변경되면, UI를 새로 만들거나 파기하지 않고, *재사용*하게끔 한다. 이 전략은 사용하는 GDI 자원을 크게 줄여, 눈에 띄는 성능 향상을 가져온다. 물론 이러한 전략이 가능한 이유는 앞절에서 설명한 바와 같이, TableItem이나 TreeItem이 data를 가질 수 있기 때문이다.

TableViewer tableViewer = new TableViewer(container, SWT.VIRTUAL);

이 성능향상 전략의 핵심 아이디어는, 보이는 부분만 UI자원을 만들고 재 사용한다는데 있다. 일반적으로 이러한 작업을 SWT 수준에서 수행하는 것은 매우 까다롭고 힘든 일이다. 업데이트 해야하는 UI의 위치를 계산하고, 새로이 연결될 모델을 계산하여 업데이트 하는 일은 만만한 일이 아니다. JFace에 대한 숙련자가 아니라면, 실제로 그러한 일들이 어떻게 SWT수준에서 구현되는지, Keving Maltby가 작성한 튜토리얼을 참조해 볼 것을 권한다.

물론 TableViewer와, TreeViewer는 SWT.VIRTUAL 플래그만 주면 마법처럼 그러한 일들을 알아서 하는 고마운 친구들이다.

해시 룩업을 이용하여 매핑 및 탐색 속도를 향상

트리뷰어나 테이블 뷰어는, 어떤 방법으로 모델을 특정할 수 있는지 알지 못하기 때문에, 일일히 아이템들을 돌아다니며 정확히 비교해 보는 방법을 쓴다. StructuredViewer.setUseHashlookup(boolean) 메서드를 이용하여 모델 비교시 hash 값을 사용하도록 설정하면, 엘리먼트(모델) 과 아이템(뷰)를 매핑하고 탐색하는 속도가 월등히 빨라진다.

TableViewer myViewer = new TableViewer(container, SWT.VIRTUAL);
myViewer.setUseHashLookup(true);  // 인풋을 주기전에 호출 해 줘야 한다.

보통은 그런 경우가 드물지만, 여러분의 모델 객체가 값 객체 처럼, 여러객체가 동일 정체성을 가질 수 있다면, 여러분의 모델은 충분히 빠르고 신뢰할 수 있을 만한 hashCode() 메서드와, equals() 메서드를 구현해야 한다.

값 객체의 예:

/* 아래의 객체들은 서로 다른 레퍼런스이며, 다른 주소를 갖지만 의미적으로 동일한 객체들이다. */
Color c1 = new Color(d, 255, 0, 0);
Color c2 = new Color(d, 255, 0, 0);
Color c3 = d.getSystemColor(SWT.COLOR_RED);

이클립스에서는 IResource, IPath등이 대표적인 값 객체이다.

LazyContentProvider

게으름뱅이 전략(Lazy Pattern)은 총 계산량이 줄거나 실질적인 성능이 빨라지는 것은 아니지만, 적절한 시점으로 로드를 분산함으로써, 사용자가 성능저하를 느끼지 못하도록 만드는 재미있는 트릭이다.

예를 들어 100장의 섬네일이 있고, 각각 클릭하면 큰 그림이 열린다고 하자. 이 때, 미리 모든 큰 그림을 로드해 두면, 프로그램이 시작할 때 사용자는 100장의 그림들이 로드되는 동안 기다리다가, 입이 툭 튀어나올 것이다.

하지만 사용자가 클릭할 때 마다, 한장의 이미지만 그때 그때 로드하게 하면, 사용자는 그 시간을 눈치채지 못하고, 즉시 작동한다고 여기게 된다. 따라서, 사용자가 100장의 그림을 전부 클릭한다고 가정했을 때, 계산량이나 성능은 동일하지만 경험적 성능에는 큰 차이가 발생하게 된다.

한줄로 요약하면: 해야 할 일은 미룰 수 있을 때 까지 최대한 미루자. -ㅂ-/

Lazy Table Viewer

그러나, 막상 레이지 컨텐트 프로바이더의 인터페이스를 열어보면 당혹스러울 것이다.

public interface ILazyContentProvider extends IContentProvider {
	public void updateElement(int index);
	
	/* 이하의 메서드는 IContentProvider에서 선언 된 것이다. */
	public void dispose();
	public void inputChanged(Viewer viewer, Object oldInput, Object newInput);
}

기존에 익숙한 IStructuredContentProvider나 ITreeContentProvider와는 전혀 다른 모양새를 하고 있다.

일반적인 컨텐트 프로바이더들은 피동적으로 JFace뷰어를 보조한다. 트리뷰어나 테이블 뷰어가 어떤 모델을 갱신하려고 할 때, 컨텐트 프로바이더에게 정보를 요청하고, 그 정보를 이용하여 뷰어가 갱신 작업을 수행한다:

진입점 -> 트리뷰어 : 업데이트 요청
트리뷰어 -> 컨텐트 프로바이더: 자식정보 요청
컨텐트 프로바이더 -> 트리뷰어 : 자식 정보
트리뷰어 -> 트리 : UI갱신

그러나 레이지 컨텐트 프로바이더들은 능동적으로 일한다. 뷰어가 갱신시점을 판단하고 만약 연결된 컨텐트 프로바이더가 레이지인 경우, 갱신 작업을 컨텐트 프로바이더에게 위임하게 된다. 따라서 컨텐트 프로바이더는 직접 UI를 갱신 해 주어야 한다. SWT 수준으로 이러한 갱신을 수행하는 것은 너무 복잡하기 때문에, JFace 트리뷰어와 테이블 뷰어는 이 때 사용할 API들을 제공한다.

진입점 -> 트리뷰어 : 업데이트 요청
트리뷰어 -> 컨텐트 프로바이더: 업데이트 요청
컨텐트 프로바이더 -> 트리 : UI 갱신

테이블 뷰어의 갱신 API:

public void setItemCount(int count);
public void replate(Object element, int index);

테이블 뷰어는 스크롤이 되거나, 명시적인 업데이트 요청을 받은 경우 레이지 컨텐트 프로바이더의 updateElement 메서드를 호출한다. 만약 스크롤이 일어나 3줄의 로우가 더 보이게 되는 상황이라면 그 일이 3번 일어난다. 이 때 item의 가상의 순서인 index를 전달 해 준다. 가상(Virtual)의 순서라고 설명한 이유는, 모델의 수만큼 아이템이 만들어지는 것이 아니라 화면에 보이는 수 만큼만 아이템이 만들어지기 때문이다.

개발자는 그러한 사실을 고려하지 않아도 되게끔 가상의 index를 이용하여 의사소통한다. 레이지 컨텐트 프로바이더는 그 위치에 표시되어야 할 모델을 계산 한 다음, TableViewer#replace(Object, index) 메서드를 이용하여 직접 업데이트 한다.

setItemCount 메서드를 이용하여, 전체 아이템(테이블 로우 UI)의 갯수를 지정할 수 있다. 실제로 그만큼 로우 UI가 만들어지지는 않으며, 단지 스크롤바를 적당하게 표시하는 데 등에 사용된다.

그러나 *가상*이든 *실제로 만들어지지 않는다는 사실*등에 유의할 필요는 없다. 그 만큼의 Item이 존재한다라고 생각하는 것이, 일을 쉽게 만들 뿐만 아니라, 이 플래그의 이름이 Virtual인 이유도 그런한 의도라고 볼 수 있다.

Lazy Tree Viewer

레이지 트리뷰어는 ILazyTreeCotnentProvider 나 ILazyTreePathContentProvider 를 컨텐트 프로바이더로 사용한다. 이 문서에서는 ILazyTreeCotnentProvider 를 기준으로 설명한다.

public interface ILazyTreeContentProvider extends IContentProvider {
	// 특정 트리 아이템과 연결된 모델을 교체해야 할 때 콜백.
	// TreeViewer#replace 를 이용해서 해당 작업을 해야 한다.
	public void updateElement(Object parent, int index);

	// 특정 노드의 자식의 수를 갱신해야 할 때 콜백 
	// 트리뷰어의 setChildCount 메서드를 이용해서 처리.
	public void updateChildCount(Object element, int currentChildCount);

	public Object getParent(Object element);

	/* 이하의 메서드는 IContentProvider에서 선언 된 것이다. */
	public void dispose();
	public void inputChanged(Viewer viewer, Object oldInput, Object newInput);
}

트리 뷰어의 레이지 컨텐트 프로바이더도, 테이블과 큰 차이는 없다. 단지 모든 부모 노드에 대해 처리할 추가적인 작업들이 몇가지 더 있다.

트리뷰어의 갱신 API

public void replace(Object parentElementOrTreePath, int index, Object element);
public void setChildCount(Object elementOrTreePath, int count);

정리

트리나 테이블의 성능을 높이는 방법을 다시 정리하면 다음과 같다.

  1. SWT.VIRTUAL 플래그를 이용하여 필요한 만큼만 Item을 만들도록 함
  2. 뷰어가 모델과 UI를 맵할 때 해시를 사용하게 함
  3. 레이지 컨텐트 프로바이더를 사용함

코드로 나타내면 다음과 같다:

TableViewer myViewer = new TableViewer(container, SWT.VIRTUAL);
myViewer.setUseHashLookup(true);
myViewer.setContentProvider(new MyLazyProvider());

3번의 경우 기존 코드 수정이 필요한데, 이것이 부담스럽다면 1, 2번만 사용해도 눈에 띄는 성능향상을 볼 수 있다. 하지만 지금 "좋아요" 버튼을 누르지 않으면 아무것도 빨라지지 않는다.

'JFace/SWT' 카테고리의 다른 글

TreeViewer, 컬럼 이동 및, 정렬 표시  (1) 2011.01.10
SWT의 사정 - 위젯 상속을 불허하는 이유  (1) 2011.01.07
SWT, JFace 그리고 Dispose  (0) 2010.11.26
SWT에서의 한영전환  (0) 2010.09.29
Canvas와 더블 버퍼링  (0) 2010.08.16
Posted by 지이이이율
,

SWT에는 크게 두 종류의 클래스들이 있습니다. 하나는 UI를 추상화하는 클래스들로 이들은 모두 Widget을 상속받습니다. 나머지는 Resource를 상속받는 클래스들로 운영체제 GDI자원을 추상화 합니다.

Widget#dispose()

위짓을 디스포즈한다는 것은, 운영체에게 해당 UI의 핸들을 돌려주는 것을 의미합니다. 운영체제는 부모의 핸들만 반환 받으면, 그 핸들의 UI에 붙어 있던 자식들도 함께 회수하기 때문에, 일일히 자식까지 디스포즈 할 필요가 없습니다.

위짓의 디스포즈 절차

개발자 -> Composite : dispose()
Composite -> Composite : dispose evnet
Composite -> 자식들 : release()
자식들 -> 자식들 : dispose event
자식들 --> 개발자 :

위에서 보이는 것과 같이, 자식들의 dispose() 메서드는 호출되지 않고, 단지 dispose 이벤트만 발송됩니다. Canvas나 Composite를 상속받는 경우, 부모가 파기될 때 dispose() 메서드가 호출될 것이라 생각하고, GDI자원들을 dispose하는 코드들을 dispose()안에 작성해두면 릭이 생기니 주의가 필요합니다. 대신 다음과 같은 형태로 디스포즈 하여야 합니다.

addDisposeListener(new DisposeListener() {	
	@Override	
	public void widgetDisposed(DisposeEvent e) {
		disposeResource();  // 이 컨트롤이 사용하는 리소스들을 dispose하는 메서드
	}
});

Resource#dispose()

리소스는 UI인 위짓과 달리, 트리 형태의 부모 자식 구조가 없습니다. 이러한 GDI자원에는 컬러, 커서, 폰트, 그래픽 컨텍스트, 폴리곤, 패턴, 텍스트레이아웃, 어파인 트랜스폼등이 있습니다. 색상을 예로 들면, 빨강이라는 색깔은 여러 UI가 함께 공유해서 사용하고 있을 수 있습니다. 따라서, 리소스들은 반드시 개발자가 불필요한 시점을 판단하여 dispose()를 직접 호출 해 주어야 합니다.

JFace

JFace를 처음 시작하는 사람들은 JFace를 SWT의 일부로 착각하는 경우가 많습니다. 예를 들어 Window나 Dialog를 SWT의 Shell과 비슷한 것으로 생각하곤 합니다. 하지만 JFace는 SWT 위짓을 이용하여 원하는 UI를 대신 만들어주는 일종의 팩토리라고 볼 수 있습니다.

예를 들어 TreeViewer는 일일히 TreeItem을 만들고 아이콘, 텍스트를 지정하는 대신, 개발자가 공급한 컨텐트 프로바이더와 레이블 프로바이더를 이용하여 그 작업을 대신 해 줍니다. 또 다른 예를 들면 Action과 같은 추상화된 비즈니스 객체를 ToolbarManager는 알아서 Toolbar와 ToolItem을 만들고 Toolitem에 눌림 이벤트를 후킹해 비즈니스 로직과 연결해 줍니다.

마찬가지로 Window나 Dialog도 개발자가 Shell을 쉽게 만들수 있도록 도와주는 클래스이며, 어떤 SWT클래스도 상속받지 않습니다. 이렇게 UI를 쉽게 만들게 해 주는 것 이외에 JFace의 또 다른 임무는 컨트롤링 코드를 쉽게 만드는 것입니다.

예를들어 Person 객체의 name이 변경되어 해당 트리 아이템에 text를 다시 지정해야 하는 경우, SWT만으로 처리하려면, 개발자는 해당 모델을 표현하는 TreeItem의 맵을 직접만들고 관리하며, 찾아내어 일일히 setText를 호출해야 합니다. 이러한 저수준 UI코드가 컨트롤링 코드에 포함되면, 가독성이 크게 훼손되고 안전한 개발을 해 나가기 어렵습니다. TreeViewer는 이런 경우 간단하게 TreeViewer#update(Object element) 메서드를 제공하여 특정 모델을 표현하는 TreeItem을 간단하게 갱신할 수 있게 해 줍니다.

정리하면 JFace의 가장 중요한 두가지 역할은:

  • SWT를 이용하여 UI를 쉽게 구성할 수 있개 해줌
  • 컨트롤러 코드를 작성을 간단하게 만들어 줌
이라고 할 수 있습니다.

JFace의 소스들을 참고하여, 여러분이 만드는 에디터나, 복잡한 컨트롤들도 유사 패턴으로 작성해 보세요. 좋은 훈련이 됩니다.

JFace의 디스포즈 패턴

모든 JFace 클래스들이 그런 것은 아니지만, 대부분의 JFace 클래스들이 SWT를 이용하여 UI를 생성하고, 그것들을 쉽게 관리하게 하는 것을 기본으로 하고 있기 때문에, JFace 객체의 생명주기는 그들이 만들어 낸 UI와 대부분 일치합니다. JFace는 UI를 만들면서 보통 많은 GDI자원들을 함께 사용합니다. 따라서 JFace 스타일의 코드들은 UI의 dispose 이벤트를 받으면 사용되었던 모든 자원을 반환합니다.

TreeViewer의 디스포즈 예

Tree -> TreeViewer : dispose event
TreeViewer -> LabelProvider  : dispose
LabelProvider -> LabelProvider : 사용된이미지 및 색상 정리
LabelProvider -> TreeViewer : 
TreeViewer -> TreeViewer : unmap 
TreeViewer --> Tree :

결론

위 예제에서도 나타나는 것처럼 JFace의 요소인 LabelProvider는 dispose() 메서드를 갖고 있지만 LabelProvider는 위짓도 리소스도 아닙니다. dispose()의 의미도 전혀 다릅니다. 이들을 착각하게 되면, 릭이나 미궁에 빠지기 십상입니다.

다시 한번 정리하면 dispose()란:

  • SWT 위짓: 자기자신과 자기자신에 붙어있는 하위 위젯들 모두 Dispose 이벤트를 발송하게 한 뒤, 최초 dispose 메서드를 호출받은 위짓만 핸들을 운영체제 반환
  • SWT 리소스: 운영체제에게 빌려온 GDI 자원을 운영체제에게 반환
  • JFace: 자기 자신이 SWT를 이용하여 UI등을 구축하는데 사용했던 모든 자원을 반환 및 정리

Posted by 지이이이율
,

SWT에서 IME의 상태는 같은 Shell안에 있을 경우 보장 받습니다. 대부분의 셀 편집이나, 텍스트 필드들은 워크벤치 윈도우의 액티브 셸안에 있기 때문에, 한영상태를 유지하는데 문제가 없습니다. 그러나 입력 다이얼로그등이 열리는 순간 한영 상태를 잃게 되어 사용자의 시간을 많이 빼았게 됩니다. 이 문제를 해결 해 봅시다.

이 테크닉은 한글IME가 있는 윈도우즈 제품군에서만 확인되었습니다. 이 코드는 반드시 운영체제가 윈도우인지 체크하고 사용하세요. 다른 운영체제에서 실험 해 보신 분은 결과를 알려주시면 고맙겠습니다.

영문인지 확인하는 방법

Shell s = ...

boolean inEngilsh = s.getImeInputMode() == SWT.NONE;

영문으로 지정하는 방법

Shell s = ...

s.setImeInputMode(SWT.NONE);

한글인지 확인하는 방법

Shell s = ...

boolean inKorean = s.getImeInputMode() == SWT.Native;

한글로 지정하는 방법

Shell s = ...

s.setImeInputMode(SWT.ALPHA | SWT.PHONETIC);

그렇습니다. 이상합니다. 상식적으로는 setImeInputMode에서 SWT.NATIVE로 지정해야 할 것 같지만, 그렇게 해 보면 부작용으로 영어및 숫자가 전각으로 입력됩니다. 물론 이상하긴 합니다만, 우리 개발자들은 한글을 쓰는 서러움이 익숙하리라 믿습니다.

Posted by 지이이이율
,

커스텀 컨트롤을 만들 때 주로, Canvas를 상속받게 되는 데, 이 컨트롤이 애니메이션 요소를 갖고 있다면, 화면이 심하게 깜박거리는 것을 볼 수 있습니다. 이와 관련한 질문에는 더블 버퍼링을 쓰라는 답변이 많고, 코드까지 딸려있는데, 그래도 플리커링은 멈추지 않습니다. Canvas는 paint이벤트를 받으면, 스타일 플래그를 대조하여 SWT.DOUBLE_BUFFERED 값이 0이면 무조건 화면을 지우고 다시 그리기 때문에, 버퍼에 랜더링된 이미지를 한번에 찍는다 해도, 그 전에 지우는 과정이 눈에 보이기 때문입니다.

아래와 같이 생성자를 변경하면, 그리기 코드에 수동 버퍼나, 오프 이미지등과 같은 테크닉을 사용하지 않더라도 자동으로 더블 버퍼됩니다.

Canvas c = new Canvas(parent, SWT.DOUBLE_BUFFERED);
Posted by 지이이이율
,