'jface'에 해당되는 글 2건

  1. 2011.05.16 SWT, 상속받지 마세요, JFace에게 양보하세요. 2
  2. 2010.11.26 SWT, JFace 그리고 Dispose

여전히 많은 이클립스 개발자들은 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 지이이이율
,

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