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

필요 선수지식

  • 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 지이이이율
,

p2

이클립스가 번들들을 설치, 업데이트 또는 관리를 할 때 사용하는 프로비져닝 플랫폼을 P2라고 합니다. 근본적으로, P2는 이클립스 또는 에퀴녹스 기반의 어플리케이션을 프로비져닝하고 관리하는 기술입니다.

Agent

클라이언트에 설치된 프로비져닝 인프라는 주로 Agent라고 언급됩니다. 에이전트는 자기자신이나 다른 프로필을 관리할 수 있습니다. 에이전트는 이클립스 시스템과 별도로 작동할 수 있으며, 다른 이클립스 시스템에 임베딩된 형태일 수도 있습니다. 에이전트는 필요한 경우 여러 프로필을 관리할 수있으며, 반대로 한 시스템이 여러개의 에이전트를 가질수도 있습니다. *P2 에이전트*라고하는 것은 실제로 존재하지 않습니다. P2란 모듈일 뿐이기 때입니다. 임베디딩 시스템이나, 데스크탑 또는 서버에서 P2 에이전트를 사용할 때 각기 다른 모듈이 사용됩니다.

Artifact

아티팩트들은 설치되거나 관리될 실제 컨텐츠를 말합니다. 번들의 JAR파일 또는 실행파일이 대표적인 아티팩트입니다.

Artifact Repository

아티팩트들을 담고 있는 리포지터리를 의미합니다.

Director

디랙터란 Planner와 Engine에서 일어나는 일들에 대한 상위 수준 API입니다. 즉, 디렉터는 플래너에게 프로비져닝 작업들을 수행하게끔 명령을 내리고, 그 결과를 엔진에게 전달하고 적용하게 하여, 필요한 프로비져닝 작업을 수행하게 합니다.

엔진

엔진은 디렉터가 결정한 필요한 프로비져닝 오퍼레이션들을 실제로 수행할 책임을 갖습니다. 디렉터의 주 작업의 주제가 메타데이터인 반면, 엔진의 관심사는 디렉터가 선정한 IU(Installation Unit)들에 포함된 아티팩트 및 구성 정보입니다. 엔진은 필요한 아티팩트를 필요한 위치에서 사용할 수 있도록 리포지터리 및 전송 매커니즘에 협조하게 됩니다.

이클립스 위키에서 엔진 보기

가비지 컬렉션

알려진 루트로 부터 접근성 추적을 통하여, 불필요한 리포지터리의 요소(메타데이터와 아티팩트)들은 수집되어 파기될 수 있습니다. 예를 들어, 에이전트에 의해 관리되는 모든 프로필들은 프로비져닝 에이전트가 직접적으로 관심을 갖는 모든 IU들을 식별할 수 있습니다. 마찬가지로 IU역시 프로필을 실행하기 위해 필요한 아티팩트들을 식별할 수 있습니다. 전이 목록에 포함되지 않은 IU나 아티팩트들은 쓰레기로 취급되며 수집됩니다.

Installation Unit(IU)

IU는 설치될 것들에 대한 정보를 담은 *메타데이터* 이며, 실제로 설치되는 것들을 의미하지 않습니다. 따라서, 번들은 IU가 아니고, 단지 번들의 이름, 버전, 캐퍼빌리티, 디펜던시 등등을 담은 디스크립션입니다. 번들의 JAR는 아티팩트입니다.

이클립스 위키에서 IU 보기

메타데이터 리포지터리

IU들을 담고 있는 메타데이터 리포지터리.

미러링

분산의 기본 오퍼레이션은 미러링입니다.

Phase

프로비져닝 오퍼레이션은 보통 여러 과정(페이즈)에 걸쳐 특정작업을 수행하던 중 일어납니다. 각 과정(Phase)마다 특정한 종류의 활동이 일어납니다. 구성 단계(Configure Phase)에 런타임 시스템과 접점(Touchpoint)의 상태에 따라, 동적으로 다양한 아티팩트를 필요로하게 될 것이고, 그에 따라 동적으로 Fetch Phrase가 수행될 것입니다.

플래너

플래너는 주어진 프로필을 요청받은대로 재 구성하는데 필요한 작업들을 결정합니다. 다시 말해, 프로필의 현재 상태와 목표 상태, 그리고 메타데이터(가용한 IU들에 대한 정보를 담은)를 이용하여 프로비져닝 오퍼레이션들이 담긴 리스트를 만들어 냅니다. (예: 인스톨, 업데이트 또는 언인스톨).

Touchpoint

P2에서 터치포인트는 P2 프로비져닝 시스템과 특정 런타임 및 관리 시스템을 통합하는 임무를 가지며, 엔진의 일부입니다. 예를 들어 이클립스 터치포인트는 에퀴녹스 스토어를 이해하고, 번들을 관리합니다. 다른 플랫폼은 다른 네이티브 터치포인트 구현을 이용하여 통합합니다. 더 다양한 터치포인트의 예제를 보려면 이곳(영문)을 클릭하십시오.

'Eclipse Core' 카테고리의 다른 글

아아 좋은 이클립스 삽질이다  (0) 2010.11.01
Extension Point and Extension  (0) 2010.07.08
어댑터 디커플링  (2) 2009.08.10
Job과 Schedule Rule  (0) 2009.08.08
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 지이이이율
,