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

베지어 곡선을 이용해서 애니메이션 커브를 줄곧 계산했었는데요, 애니메이션 커브에서 베지어 연산은 좀 비효율적인 것 같아서, 그래퍼로 좀 정리를 했습니다. 위에서 부터 차례대로 다음과 같으며 꽤 괜찮은 성능을 보이는 함수 입니다.

  • Ease In
  • Ease Out
  • Ease In Out
Posted by 지이이이율
,

XML 1.1을 사용하면 1.0에서는 사용할 수 없던 제어문자를 어트리뷰트 값으로 사용할 수 있기 때문에, XML1.1로 변경하는 작업을 최근 진행했다. 그러자 이상하게도 간헐적으로 EMF가 모델을 XML로 부터 읽어들이지 못하는 문제가 발생했다.

주요 증세

  • PackageNotFound 예외
  • 패키지 URI가 널일 수 없다는 예외
  • 이상한 명칭의 EClass 를 찾지 못했다는 예외

원인

사실 이 문제가 XML 버전과 연관이 있다는 사실을 알아내는데만도 반나절 이상의 시간을 소비해야 했다.

문제는 JVM에 기본 포함된 저시스 SAX파서가 XML 1.1의 경우 몇몇 어트리뷰트를 토큰 레벨에서 부터 제대로 스캔하지 못하는 경우가 발생했기 때문이었다. 이로인해 EMF XML 계층에 잘못된 Sax Event가 전달되어, 그 중에서도 xsi:type 어트리뷰트 값이 엉뚱한 값이 들어가 타입명으로 부터 EClass를 찾지 못하거나, 어트리뷰트 값으로 부터 패키지 URI를 파싱하지 못해 문제가 발생한다.

이 문제는 EMF의 XMLLoadImpl을 개인화 하여, 파서를 교체하여 해결 할 수 있다. 필자의 경우, 문제를 해결하기 위해 저시스 2.11 (XML 1.1스키마용 베타) 버전을 다운 받아 빌드 패스에 넣고, 아래와 같이 XMLLoadImpl을 개인화하여 문제를 수정했다.

수정 코드:

public class MyXMLLoadImpl extends XMILoadImpl {
	@Override
	protected SAXParser makeParser() throws 
		ParserConfigurationException, SAXException {
		SAXParserFactory f = 
			SAXParserFactory.newInstance(
				SAXParserFactoryImpl.class.getCanonicalName(), 
				getClass().getClassLoader()
			);
		return f.newSAXParser();
	}
}

EMF 로딩 커스터마이징 하기도 함께 읽어 보세요.

'EMF' 카테고리의 다른 글

How to Customize EMF Loading  (0) 2010.09.27
EMF 트랜잭션 관리  (0) 2009.06.08
Posted by 지이이이율
,