지난 시간에는 GEF 어플리케이션을 작성하기 위해 Plug-in Project를 생성했다. GEF 어플리케이션은 RCP에 기반을 두고 있으므로 이클립스 플러그인으로 개발되어야 하며, 실행기를 포함시키기 위해 Rich Client Application 항목을 Yes로 설정한 걸 기억하자.

▶ Model-View-Controller Architecture

GEF는 전형적인 MVC 구조로 구성된다. 다루고자 하는 정보는 모델로써 정의되며, 이에 대한 뷰어가 지정되어 해당 모델을 화면에 표시한다. 표시된 뷰어를 통해 사용자는 일련의 조작(이동, 크기 변경, 속성 변경, 모델 추가 등)을 할 수 있는데 이러한 조작은 컨트롤러에 의해 동작하며 이 동작은 모델을 갱신하고, 이로 인해 뷰어가 갱신된다.

얼핏보면 왜 이렇게 힘들게 만드나 싶을 수도 있다. 그냥 데이터를 갖는 객체가 직접 자신을 그리고 조작 행위를 구현하면 되지 않겠는가?

실질적인 데이터를 나타내는 모델과 이에 대한 뷰, 그리고 그 사이의 컨트롤러를 분리하는 이유는 보다 유연한 시스템을 구성하기 위해서다. 이러한 분리로 인해 각각의 요소는 유일한 목적에 의해 구현된다. 예를 들어 모델의 경우, 모델은 데이터와 그것의 조작만을 담당할 뿐 이것이 어떻게 그려져야 하는지에 대해서는 전혀 고려할 필요가 없다. 따라서 각 요소의 재사용성이 높아지고 변경에 쉽게 적응할 수 있다.

GEF에서 모델은 일반적인 자바 객체로 구현된다. 별도의 클래스를 상속하거나 인터페이스를 구현할 필요가 없으므로 모델만큼은 굉장히 독립적으로 유지할 수 있다. (다른 시스템에서 사용하던 모델을 그대로 사용할 수도 있다) 그러나 몇가지 제약이 있다. 첫째로 모든 모델은 그것의 유일한 루트가 되는 모델이 존재해야 한다. XML과 같은 트리 구조를 생각하면 된다. 루트 엘리먼트가 필요하다는 이야기다. GEF에서 최상위 모델은 삭제될 수 없다. 둘째로 모델이 변경되었을 때 이에 대해 통지해야 한다. 모델이 변경된 것을 자기 자신만 알고 있으면 뷰는 어떻게 갱신하겠는가? 이런 저런 이유로 모델의 변경이 발생하면 이를 통지해야 하며 이는 일반적인 옵저버 패턴으로 구현된다.

GEF에서 뷰는 SWT나 Draw2D를 통해 구현된다. 모델의 뷰는 유일할 필요가 없다. 편집창에서 아이콘 형식으로 보여질 수도 있고, 트리 뷰어를 통해 트리 아이템으로 보여질 수도 있다.

GEF에서 컨트롤러는 EditPart를 통해 구현된다. EditPart는 모델과 뷰를 대응시키며 모델의 추가/삭제/변경, 뷰의 선택이나 드래그 이벤트와 같은 어떤 행위에 대한 책임을 갖는다. (이러한 책임에는 모델이 변경되었을 때 해당 뷰를 다시 그리는 것을 포함한다) 이 모든걸 EditPart 내에 구현한다고 생각하면 끔찍할 것 같지만 실제로는 상당 부분이 GEF에 의해 미리 구현되어 있으며, EditPolicy를 통해 이러한 행위가 '설치' 된다.

▶ EditPolicy를 좀 더 알아보자.

GEF는 실질적인 편집 환경이므로 모델의 변경과 이에 대한 리액션에 상당한 비중이 있다. 이러한 리액션은 EditPolicy로 구현되어 EditPart에 설치된다. EditPart는 모델의 수정, 뷰의 위치 변경 등과 같이 다양한 역할(role)을 가지며, EditPolicy는 그것의 역할에 따라 설치된다. 역할은 String 식별자로 지정되며 GEF에서는 기본적인 역할에 대해 EditPolicy.COMPONENT_ROLE과 같이 식별자를 미리 정의하고 있다.

살펴본 바와 같이 EditPolicy는 어떤 이벤트/행위에 대한 리액션을 담당한다. 그렇다면 이러한 리액션에 필요한 정보는 어디로부터 오는가? 리액션에 필요한 정보는 Request 객체로 전달된다. EditPolicy가 Request 객체를 받으면 이를 해석하여 적절한 리액션을 수행하는 Command 객체를 반환한다. 이것은 Command 스택에 놓여지고 GEF 프레임워크에 의해 순차적으로 수행된다. Command 스택은 일종의 history 역할을 수행하므로 undo/redo와 같은 기능을 쉽게 구현할 수 있게 한다.

▶ GEF 어플리케이션을 작성하기 전에...

우리가 생성한 plug-in project에서 GEF를 사용할 수 있도록 GEF 플러그인을 추가해야 한다. plugin.xml을 더블클릭하여 Overview 편집창을 띄우고 하단의 Dependencies 탭을 클릭하면 다음과 같은 화면을 볼 수 있다.

Overview 편집창 하단의 Dependencies 탭을 클릭하자


왼쪽 박스(Required Plug-ins)에서 Add... 버튼을 클릭하고 GEF 플러그인을 추가하자.

org.eclipse.gef를 선택한다.


그러면 다음과 같이 GEF 플러그인이 추가된 것을 볼 수 있다. Ctrl+S를 누르거나 File-Save를 선택하여 plugin.xml을 저장하자.

GEF 플러그인이 추가된 것을 확인했으면 반드시 plugin.xml을 저장할 것!


▶ 모델/뷰/컨트롤러를 작성하자.

오늘 배운 내용을 기반으로 모델과 뷰, EditPart를 생성해보자. 우선 모델을 작성해야 한다. 본 튜토리얼에서는 연구실 조직 관리 어플리케이션을 작성할 것이므로 루트 모델로써 대학(University) 모델 클래스을 작성한다.
package semix2.tutorial.gef.model;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
public class University {
	public static final String PROPERTY_NAME		= "University.NAME";
	public static final String PROPERTY_ADDRESS		= "University.ADDRESS";
	
	private String					_name;
	private String					_address;
	private PropertyChangeSupport	_listeners;
	
	public University() {
		_listeners = new PropertyChangeSupport(this);
	}
	
	public void setName(String name) {
		String oldName = _name;
		_name = name;
		
		// 모델의 변경에 대해 listener들에게 알린다.
		_listeners.firePropertyChange(PROPERTY_NAME, oldName, name);
	}
	
	public String getName() {
		return _name;
	}
	
	public void setAddress(String description) {
		String oldDescription = _address;
		_address = description;
		
		// 모델의 변경에 대해 listener들에게 알린다.
		_listeners.firePropertyChange(PROPERTY_DESCRIPTION, oldDescription, description);
	}
	
	public String getAddress() {
		return _address;
	}
	
	public void addListener(PropertyChangeListener listener) {
		_listeners.addPropertyChangeListener(listener);
	}
	
	public void removeListener(PropertyChangeListener listener) {
		_listeners.removePropertyChangeListener(listener);
	}
}
무엇이 되었든 모델의 변경에 관심이 있는 녀석들은 PropertyChangeListener를 구현하여 모델에 추가하면 모델이 변경되었을 때 통지받을 수 있다. 이것을 구현하기 위해 반드시 PropertyChangeSupport를 사용할 필요는 없다.

모델을 만들었으니 이번엔 뷰를 구현해보자. 뷰는 Figure 객체를 확장하여 구현할 것인데 Figure는 편집창 중앙에서 보여질 모델의 이미지라고 생각하면 된다. figure 패키지를 생성하고 다음의 코드를 작성하자.
package semix2.tutorial.gef.figure;
import org.eclipse.draw2d.ColorConstants;
import org.eclipse.draw2d.Figure;
import org.eclipse.draw2d.Label;
import org.eclipse.draw2d.LineBorder;
import org.eclipse.draw2d.XYLayout;
import org.eclipse.draw2d.geometry.Rectangle;

public class UniversityFigure extends Figure { private final Label _labelName; private final Label _labelAddress; public UniversityFigure() { _labelName = new Label(); _labelAddress = new Label(); init(); } private void init() { setLayoutManager(new XYLayout()); // University 이름 라벨을 구성한다. _labelName.setForegroundColor(ColorConstants.black); add(_labelName); setConstraint(_labelName, new Rectangle(5, 5, -1, -1)); // University 주소 라벨을 구성한다. _labelAddress.setForegroundColor(ColorConstants.lightGray); add(_labelAddress); setConstraint(_labelAddress, new Rectangle(5, 20, -1, -1)); setForegroundColor(ColorConstants.black); setBorder(new LineBorder(3)); } public void setName(String name) { _labelName.setText("학교: " + name); } public void setAddress(String address) { _labelAddress.setText("주소: " + address); } }
코드에서와 같이 UniversityFigure는 University 모델의 데이터를 표현하기 위한 뷰를 구현한다. Figure는 모델 객체를 직접 소유하지 않으며 이들간의 연계는 EditPart가 담당한다. 이제 EditPart를 작성하자.
package semix2.tutorial.gef.editpart;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import org.eclipse.draw2d.IFigure;
import org.eclipse.gef.editparts.AbstractGraphicalEditPart;
import semix2.tutorial.gef.figure.UniversityFigure;
import semix2.tutorial.gef.model.University;

public class UniversityEditPart extends AbstractGraphicalEditPart implements PropertyChangeListener {

	@Override
	public void activate() {
		// University 모델 객체의 변경을 통지 받기 위해 listener를 등록한다.
		((University)getModel()).addListener(this);
	}

	@Override
	public void deactivate() {
		// 등록한 listener를 해지한다.
		((University)getModel()).removeListener(this);
	}

	@Override
	protected IFigure createFigure() {
		// Figure를 생성한다.
		return new UniversityFigure();
	}

	@Override
	protected void createEditPolicies() {
		// EditPolicy를 설치한다.
		// 아직까지는 별다른 EditPolicy를 설치하지 않는다.
	}

	@Override
	protected void refreshVisuals() {
		// 뷰를 갱신한다.
		UniversityFigure figure = (UniversityFigure)getFigure();
		University model = (University)getModel();
		figure.setName(model.getName());
		figure.setAddress(model.getAddress());
	}

	@Override
	public void propertyChange(PropertyChangeEvent evt) {
		// University 모델 객체의 변경이 통지되면 이에 대한 적절한 반응을 해야 한다.
		// 모델 속성의 변화는 Figure를 갱신해야 하므로 refreshVisuals()를 호출한다.
		if (evt.getPropertyName().equals(University.PROPERTY_NAME)) {
			refreshVisuals();
		} else if (evt.getPropertyName().equals(University.PROPERTY_ADDRESS)) {
			refreshVisuals();
		}
	}
}
마지막으로 모델에 대한 EditPart를 생성하기 위해 EditPartFactory를 작성해야 한다.
package semix2.tutorial.gef.editpart;
import org.eclipse.gef.EditPart;
import org.eclipse.gef.EditPartFactory;
import org.eclipse.gef.editparts.AbstractGraphicalEditPart;
import semix2.tutorial.gef.model.University;

public class LabEditPartFactory implements EditPartFactory {

	public EditPart createEditPart(EditPart context, Object model) {
		AbstractGraphicalEditPart part = null;
		if (model instanceof University) {
			part = new UniversityEditPart();
		}
		part.setModel(model);
		return part;
	}
}
▶ 최초의 GEF 어플리케이션 작성

모델/뷰/컨트롤러가 완성되었다. 이제 GEF 어플리케이션을 작성해보도록 하자. 시각적 편집 환경은 GraphicalEditor를 확장하여 구현한다. RCP에서 editor는 org.eclipse.ui.editors 확장 포인트로 추가되는데 String 식별자를 반드시 가져야하며, 이후 이 식별자를 통해 editor를 접근할 수 있다. 편의를 위해 코드 내에 static final 필드로 id를 정의하는 것이 좋다. 그럼 Editor를 작성해보자.
package semix2.tutorial.gef;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.gef.DefaultEditDomain;
import org.eclipse.gef.ui.parts.GraphicalEditor;
import semix2.tutorial.gef.editpart.LabEditPartFactory;
import semix2.tutorial.gef.model.University;

public class LabGraphicalEditor extends GraphicalEditor {
	public static final String ID = "semix2.tutorial.gef.LabGraphicalEditor";
	
	public LabGraphicalEditor() {
		setEditDomain(new DefaultEditDomain(this));
	}
	
	private University createUniversity() {
		// EditorInput을 기반으로 초기 모델을 구축한다.
		University university = new University();
		university.setName(getEditorInput().getName());
		university.setAddress("아직 지정되지 않았습니다");
		return university;
	}
	
	@Override
	protected void initializeGraphicalViewer() {
		// 편집창의 최초 편집 환경을 설정한다.
		getGraphicalViewer().setContents(createUniversity());
	}
	
	@Override
	protected void configureGraphicalViewer() {
		// LabEditPartFactory를 등록한다.
		getGraphicalViewer().setEditPartFactory(new LabEditPartFactory());
	}

	@Override
	public void doSave(IProgressMonitor monitor) {
		
	}
}
RCP에서 editor는 EditorInput에 의해 구동된다. 이미 열려있는 editor가 있는데 동일한 EditorInput으로 editor를 열려고 하면 기존의 editor에 포커스를 옮긴다. (다시 말해 같은 EditorInput에 대해 둘 이상의 Editor를 생성하지 않는다) EditorInput을 구현하자. 이 때 반드시 equals() 메소드를 재정의하여 동일한 EditorInput 인지 검사하는 루틴을 작성하여야 한다.
package semix2.tutorial.gef;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IPersistableElement;

public class LabEditorInput implements IEditorInput {
	private final String			_name;
	
	public LabEditorInput(String name) {
		_name = name;
	}
	
	@Override
	public boolean exists() {
		return (_name != null);
	}

	@Override
	public ImageDescriptor getImageDescriptor() {
		return null;
	}

	@Override
	public String getName() {
		return _name;
	}

	@Override
	public IPersistableElement getPersistable() {
		return null;
	}

	@Override
	public String getToolTipText() {
		return _name;
	}

	@Override
	public Object getAdapter(Class adapter) {
		return null;
	}
	
	@Override
	public boolean equals(Object object) {
		// 동일한 EditorInput인지 검사하는 루틴을 작성한다.
		// LabEditorInput은 _name 값이 같으면 동일한 것으로 간주한다.
		if (!(object instanceof LabEditorInput)) {
			return false;
		}
		return ((LabEditorInput)object).getName().equals(_name);
	}
}
Editor와 EditorInput을 작성했으니, 이제 RCP가 작성한 editor를 사용할 수 있도록 extension을 등록해야 한다. plugin.xml을 더블클릭하여 Overview 편집창을 띄우고 Extensions 탭을 선택하면 Extension 편집 화면이 뜬다. 여기서 Add 버튼을 클릭하여 org.eclipse.ui.editors 확장 포인트(Extension Point)를 추가하자.

org.eclipse.ui.editors 확장 포인트를 추가하자


그리고나서 Extension Element Details를 수정하자. 먼저 id 필드에는 LabGraphicalEditor.ID 값을 적는다. (글자 하나라도 틀려서는 안된다!) name 필드에는 간단히 이름을 적고, class 필드에는 아까 작성한 LabGraphicalEditor 클래스를 지정한다. (옆의 Browse 버튼을 이용하면 쉽게 할 수 있다) 여기까지 제대로 따라했다면 다음과 같이 작성되었을 것이다.

LabGraphicalEditor를 org.eclipse.ui.editor 확장 포인트로 추가한다.


이제 어플리케이션이 시작되었을 때 LabGraphicalEditor가 시작되도록 다음과 같이 ApplicationWorkbenchAdvisor에 postStartup() 메소드를 재정의하자.
package semix2.tutorial.gef;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.application.IWorkbenchWindowConfigurer;
import org.eclipse.ui.application.WorkbenchAdvisor;
import org.eclipse.ui.application.WorkbenchWindowAdvisor;
public class ApplicationWorkbenchAdvisor extends WorkbenchAdvisor {

	// 중간 생략

	@Override
	public void postStartup() {
		// 어플리케이션의 시작 준비가 완료되면 LabGraphicalEditor를 띄운다.
		try {
			IWorkbenchPage page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage();
			page.openEditor(new LabEditorInput("서울시립대학교"), LabGraphicalEditor.ID, true);
		} catch(Exception e) {
			e.printStackTrace();
		}
	}
}
이제 실행해보자. 여기까지 제대로 작성했다면 다음과 같은 화면을 볼 수 있을 것이다.

GraphicalEditor가 생겨났고 여기에 University 모델이 그려진 모습이다.



▶ 오늘의 정리

GEF 어플리케이션을 작성하기 위해서는 MVC 패턴에 따라 모델과 뷰(이번 강좌에서는 Figure를 사용하였다), 컨트롤러(EditPart)를 구현해야 한다. 모델은 일반적인 자바 클랙스로 정의되지만 루트 모델이 반드시 하나 존재해야 하며, 속성의 변화를 알릴 수 있도록 옵저버 패턴을 구현해야만 한다. 시각적 편집 환경을 구현하기 위해 AbstractGraphicalEditor를 확장하였으며 구현한 Editor 객체를 org.eclipse.ui.editors 확장 포인트로 추가하였다.



CATEGORIES