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

필요 선수지식

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