오늘은 다른 사람이 만든 커맨드를 수행하되, 워크밴치 셀렉션을 직접 바꾸지 않고도 개발자가 원하는 셀렉션 모델에 대해 그 비즈니스 로직이 수행되게 하는 방법을 배워 보겠습니다.

커맨드의 수행 과정

CCI -> 핸들러 서비스: 커맨드 제출
핸들러 서비스 -> 평가 서비스: 컨텍스트 요청
평가 서비스 -> 핸들러 서비스: 컨텍스트 공급
핸들러 서비스 -> 핸들러 서비스: 핸들러 결정
핸들러 서비스 -> 핸들러: 실행 이벤트
핸들러 -> 핸들러 : 비즈니스 로직 수행

CCI (커맨드 컨트리뷰션 아이템)

JFace 를 공부하신 분이라면 ActionContributionItem(ACI) 을 기억 하실 겁니다. ACI가 액션을 툴바, 메뉴, 컴포지트등에 표현하고 액션의 상태가 변경될 때마다 표현한 UI를 업데이트하며, 사용자가 UI를 누르면 Action 을 수행하게 하는 임무를 갖고 있는 것 처럼, CCI는 커맨드를 UI에 기여하고 업데이트 시키며, UI가 눌렸을 때 커맨드를 수행합니다.

단, 액션은 비즈니스 로직을 직접 가진 반면, 커맨드는 스트링일 뿐입니다.

커맨드가 커맨드 플랫폼에 제출 되면, 핸들러 서비스는 현재 상황 (IEvaluationContext)에 따라 적절한 핸들러를 선택하고, 커맨드를 처리하게 합니다.

핸들러들은 확장을 통해 기술 되는데 특정 뷰가 활성 상태일 때, 또는 워크벤치의 선택 모델이 특정 타입일 때와 같은 특정 문맥(context)에서 활성화 되고 enable 되어야 한다는 선언(코어 익스프레션)도 함께 가지고 있습니다. 핸들러가 결정되면 핸들러의 execute(ExecutionEvent)가 호출되고, 이 안에 비즈니스 로직이 기술됩니다.

CCI들은 이러한 문맥에 따라 가용한 핸들러가 없는 경우, 기여한 UI를 disable 시키는 등의 작업도 수행합니다. 이렇게 커맨드가 화면상에 표현된 것들을 UIElement 라고 부릅니다.

핸들러

핸들러는 실행 될 때 ExecutionEvent를 공급 받습니다. 이 안에는 어플리케이션의 현재 문맥 상태, 파라미터 값과 같은 여러 정보가 들어 있으며 핸들러의 구현자는 HanlderUtil을 이용하여 이 정보를 보다 쉽게 억세스 할 수 있습니다.

우리의 목표는 핸들러의 비즈니스 로직을 이용하되, 핸들러가 워크벤치 셀렉션을 처리하는 것이 아닌 우리가 원하는 모델을 처리해 주길 원하는 것입니다. 그러나 핸들러와 커맨드는 우리가 만든 것이 아니므로, 내부 구현을 바꾸지 않고 이를 달성할 가장 좋은 방법은 핸들러의 입력 모델인 Execution Event 를 조작하여 공급하는 것입니다.

코드

IHandlerService handlerService =
	 (IHandlerService) PlatformUI.getWorkbench().getService(IHandlerService.class);

ParameterizedCommandFactory factory 
	= new ParameterizedCommandFactory("someCommandId");

ParameterizedCommand command = factory.buildCommand();

IEvaluationService evaluationService = (IEvaluationService) 
	PlatformUI.getWorkbench().getService(IEvaluationService.class);

IEvaluationContext state = evaluationService.getCurrentState();
	
StructuredSelection selection = // 셀렉션을 만듬
state.addVariable("selection", selection);

handlerService.executeCommandInContext(command, null, state);

4~7 라인은 수행할 커맨드 ID를 이용하여 커맨드를 만드는 과정입니다.

ParameterizedCommandFactory는 파라미터 커맨드를 쉽게 만들 수 있게 해주는 유틸리티로, 이클립스의 기본 요소가 아닙니다. 여기에서 다운로드 받을 수 있습니다.

9~15 라인은 해당 커맨드가 수행될 환경을 조작하는 작업입니다.

우선 평가 서비스로 부터 현재 상태를 얻어 selecton의 값을 우리가 원하는 형태로 고쳐 씁니다. 만약 핸들러가 <with />익스 프레션으로 다른 변수를 접근하여 enableWhen이나 activeWhen에 사용한다면 그런 변수도 여기서 줄 수 있습니다.

17 라인에서는 조작된 환경을 바탕으로 커맨드를 제출 하여 수행하게 합니다. 두번째 인자가 null인 이유는 원래 이 자리엔 최초 이 일이 발생시킨 원인인 UI Event를 넣는 자리인데, 이 경우 프로그래밍으로 커맨드를 수행하는 것이므로, 이벤트가 따로 존재하지 않습니다.

의미와 응용

Action 개체는 UI와 비즈니스 로직의 결합도가 굉장히 심합니다. ACI계층이 있어도 이 ACI는 Action으로 부터 텍스트, 레이블, 아이콘등을 가져오기 때문에, 초기 화면을 구성할 때 비즈니스 로직이 담긴 Action 클래스들을 모두 로드해야 합니다. 반면 Action은 run 을 직접 호출함으로써 개발자는 손 쉽게 비즈니스 로직만 리유즈가 가능하며, 해당 액션의 필드를 변경하여 동작을 수정해가며 재사용 가능합니다. (물론 이 Action 클래스에 대한 가시성이 있다는 전제하에)

그러나 핸들러는 기본적으로 비공개성향 (대부분 인터널 패키지)이 강하고 커맨드 ID만 외부에 노출되어 입맛에 맞게 그 비즈니스 로직을 리유즈 하는 것이 난해합니다. 이 포스트에서 익힌 테크닉을 이용하면 네비게이터에서 숨겨져 보이지 않는 파일을 선택하고 삭제 명령을 수행한다던가, GUI테스트 시에 목업으로 사용한다던가, 특정 핸들러가 다른 핸들러들을 순차적으로 이용하여 데이터를 가공해 나간다던가 하는 식의 응용이 가능할 것입니다.

'PDE' 카테고리의 다른 글

빌드 자동화 하기 #1 - Hello Ant  (2) 2011.03.07
이클립스 플러그인에 DLL 포함시키기  (0) 2010.12.10
제품 빌드시 자주 발생하는 문제들  (0) 2010.11.26
Bundle과 Resource  (0) 2010.10.28
네이쳐와 빌더  (0) 2010.10.15
Posted by 지이이이율
,

이 씨리즈의 문서는 이클립스 피쳐 프로젝트를 자동 빌드하고, 서명, 업데이트 사이트 및 리포지터리를 자동으로 업데이트 하는 방법을 익히게 하는 것을 목적으로 합니다. 초보적인 Ant이해를 가진 이클립스 플러그인 개발자를 대상으로 합니다.

ANT 파일 만들기

우선 빌드를 위한 빈 자바 프로젝트를 HelloAntBuild라는 이름으로 만듭니다. 자바 프로젝트로 만드는 이유는, Ant스크립트로 처리할 수 없는 일괄작업을 Java로 기술해야만 하는 경우를 대비하기 위해서 입니다.

그리고 HelloAnt.build.xml 라는 파일을 만들고 다음의 내용을 입력합니다:



	 
		Hello Ant!
	

XML 또는 텍스트 에디터를 닫고, 이 파일을 Ant Editor로 엽니다. 아웃라인 뷰에서, build를 우클릭한 뒤, [Run As] / [Ant Build] 를 차례로 선택하여, 화면에 Hello Ant!가 잘 찍히는 지 확인합니다.

수행결과

Eclipse Ant Task 사용하기

이클립스에서는 Ant에서 사용할 수 있는 유용한 태스크 태그(동작)와 프로퍼티(변수)들을 공급합니다. 단, 이를 사용하기 위해서는 앤트가 실행중인 이클립스와 동일한 JVM에서 수행되어야 합니다. 아웃라인 뷰에서 build를 우클릭한 뒤, [Run As] / [Ant Build ...] 을 선택하여, 앤트 실행 구성 편집기를 엽니다.

JRE 탭에서 Run in the Same JRE as the workspace를 선택합니다.

이 앤트 파일의 실행 구성을 팀원들과 공유하기 위하여, Common 탭 Save as 항목에서 실행 구성이 저장될 위치 (워크 스페이스 내 현재 프로젝트)를 선택해 줍니다. 앞으로 이 프로젝트를 체크아웃 받아 빌드를 실행하는 다른 사람도, 자동으로 이 실행 구성을 이용하게 됩니다.

이제 Run 또는 Debug 버튼을 눌러 앤트를 실행시켜보면, 이전과 마찬가지 실행결과를 보여주지만, 네비게이터를 살펴보면 프로젝트 안에 "프로젝트이름 HeloAnt.build.xml.launch" 라는 파일이 하나 생겨 있을 것입니다. 이 파일이 앤트 실행 구성을 담고 있습니다.

이클립스 프로퍼티 사용해 보기

앤트 파일을 다시 열고, 다음의 내용을 추가합니다:


	현재 실행중인 이클립스의 위치는 ${eclipse.home.location} 입니다.

이제 이 타깃을 실행 해 봅시다. 현재 실행중인 이클립스의 위치가 보인다면 잘 따라오고 계신겁니다 :)

플러그인 빌드 하기

하나의 플러그인이 빌드 되는 과정은 매우 단순합니다.

  1. 플러그인 디펜던시에 있는 모든 플러그인과, 그 플러그인들의 디펜던시를 모두 얻어 클래스 패스에 추가한 상태로, 모든 자바 파일을 컴파일 한다
  2. build.properties 및 manifest.mf 파일의 내용에 따라, 컴파일 결과물 및 리소스를 jar파일에 집어 넣는다.
  3. 필요한 경우 완성된 jar 파일을 서명

말은 쉽지만, 1번 단계부터 막막할 겁니다.

build.properties와, manifest.mf파일을 자동으로 분석해서 해야할 일을 자동으로 생성해 주는 녀석이 있다면 아주 편리하겠죠? 다행히 eclipse.buildScript 라는 태스크가 그 일을 지원합니다. 우리는 이 태스크 태그를 이용하여, 플러그인을 빌드하는 스크립트를 생성 한 뒤에 실행 할 것입니다. 이 전략을 그림으로 그려보면 다음과 같습니다:

Main Script -> Sub Script* : generate
Sub Script* -> Sub Script* : run
Main Script -> Sub Script* : delete

연습용 플러그인 만들기

이제 자동빌드를 연습해볼 마루타 프로젝트인 HelloAntPlugin 플러그인 프로젝트를 하나 만듭니다. 특별히 아무런 구현이나 내용이 없어도 됩니다.

플러그인 프로젝트를 만들었으면, 빌드 프로젝트의 HelloAnt.build.xml을 다시 열어 build-plugin-test라는 타깃을 앤트파일에 추가하고 아래의 내용을 채워 넣으세요:


	<eclipse.buildScript 
		elements="plugin@HelloAntPlugin" 
		baselocation="${eclipse.home}" 
		builddirectory="${basedir}/../" />
	<eclipse.refreshLocal resource="/" depth="infinite" />

eclipse.buildscript 태스크는 다음과 같은 속성을 가집니다:

  • elements : 앤트 빌드 스크립트를 생성할 대상. 타입@ID 형태로 지정합니다. 우리가 빌드할 것은 플러그인이고, 아이디는 HelloAntPlugin이므로 plugin@HelloAntPlugin이 됩니다.
  • baselocation : 대상을 빌드하는데 필요한 디펜던시를 어디에서 부터 얻을지 알려줍니다. 설치된 이클립스의 경로를 알려줬습니다. 개발 타겟이 다른경우, 이곳에 타겟 이클립스의 경로를 지정하면 됩니다.
  • builddirectory : 빌드 대상 요소를 찾는 공간을 의미합니다. ${basedir}은 기본값으로 실행될 앤트파일이 담긴 디렉토리가 되는데, 그것의 부모폴더로 기술했으므로 워크스페이스의 위치가 될 것입니다. (빌드 프로젝트의 부모는 워크스페이스이므로)

나머지 자세한 속성들은 이클립스 온라인 도움말에서 확인하십시오.

eclipse.refreshLocal 태그는 워크스페이스를 갱신하는 역할을 합니다. 앤트는 이클립스와 독립적인 플랫폼이어서 IResource를 이용하여 작업하지 않습니다. 표준 File API 나 커맨드 라인 명령도구를 이용하므로, 워크스페이스의 싱크가 깨집니다. 빌드 작업이 끝나면 꼭 이 태스크 태그를 이용하여 워크스페이스를 갱신 시켜 주어야 합니다.

새로 추가한 타겟을 실행해 보면, HelloAntPlugin 프로젝트 밑에 build.xml 과 javaCompiler...args 파일이 추가된 것을 알 수 있습니다.

생성된 스크립트 실행 해 보기

생성된 build.xml을 열어 보면 뒷골이 당겨 올 겁니다. 이 앤트스크립트를 역공학 하실 수 있는 분은 시도해 보시기 바랍니다.

생성된 build.xml을 열어서, 아웃라인 뷰에서 "@dot"를 선택하고 우클릭 한 뒤, [Debug As] / [Ant Build ...]을 선택 한 뒤, 실행 구성에서 앞서 말씀드린 것 처럼 Workspace 와 동일한 JRE를 사용하게 한 상태에서 실행 해 봅시다. 이 타깃은 소스를 컴파일 하는 역할을 합니다.

실행이 끝나면 이번에는 build.update.jar를 선택 한 뒤 다시 실행합니다.

실행이 완료되면, 플러그인 프로젝트를 우클릭 하고, [Refresh]를 선택하여 워크스페이스를 갱신합니다. eclipse.buildScript가 생성한 스크립트는 무인 실행용이어서, 사람이 결과물을 확인하지 않기 때문에 eclipse.refreshLocal을 자동으로 호출해주지 않습니다. 워크스페이스를 갱신해보면 위 그림처럼, 플러그인이 빌드된 jar파일과, 각 컴파일된 자바 클래스 파일이 구조대로 담긴 @dot 폴더가 생겼을 것입니다.

지금은 우리가 생성된 스크립트를 사람 손으로 실행 했지만, 이번에는 메인스크립트가 스크립트를 생성한 뒤 자동으로 실행하게 해 봅시다.

다른 스크립트 호출하기

이제 원래 빌드프로젝트로 돌아와 HelloAnt.build.xml을 다시 열고 build-plugin-test-2라는 타깃을 만들고 다음의 내용을 추가합니다:

 

	스크립트 생성중
	<eclipse.buildScript elements="plugin@HelloAntPlugin" baselocation="${eclipse.home}" builddirectory="${basedir}/../" />

	스크립트 실행중
	<ant antfile="../HelloAntPlugin/build.xml" target="@dot" inheritall="false" />
	<ant antfile="../HelloAntPlugin/build.xml" target="build.update.jar" inheritall="false" />
	<eclipse.refreshLocal resource="/" depth="infinite" />

ANT 태스크 태그

ant 태그의 속성은 매우 직관적입니다. antfile은 실행할 팔일을, 그리고 타깃을 지정했습니다. inheritall의 값으로 false를 줬는데, 이것은 메인스크립트에 선언된 프로퍼티를 호출될 스크립트에게 전달하겠느냐는 의미가 됩니다. 자식스크립트는 ${basedir}이 자기자신이 있는 곳이라고 생각하고 작성되었을 것이기 때문에, 보통은 프로퍼티를 상속해 주는 것은 위험합니다. 명시적으로 필요한 속성만 상속하는 것이 좋습니다. 이 방법은 나중에 소개합니다.

새로만든 타깃을 실행 해 보면, 플러그인 프로젝트 밑에 jar가 생성되는 것을 확인 할 수 있습니다.

수고하셨습니다, 다음 시간에는 feature.xml로 부터 모든 플러그인을 자동빌드하고 피쳐역시 빌드하는 방법과, 찌꺼기 파일들을 안전하게 제거하는 방법에 대해 알아보겠습니다.

좋아요를 누르지 않으면 이 씨리즈의 다음 문서가 업데이트 되지 않을 수도 있습니다.

Posted by 지이이이율
,

메뉴 컨트리뷰션을 이용하여 커맨드를 기여 한 경우, CommandContributionItem이 메뉴 매니저나, 툴바 매니저등에 포함되어 해당 커맨드를 기여하게 됩니다. 이 때, 스타일이 SWT.DROP_DOWN 인 경우, 이 기여의 ID가 메뉴 아이디로 사용될 수 있게 됩니다.

문제는 이 주소를 통해 기여한 커맨드 역시, CommandContributionItem을 통해 기여 되는 데, 이들은 영영 dispose 되지 않는 다는 점입니다. 이 문제는 CommandContributionItem#openDropDownMenu() 를 보면 더 확실해 집니다.

/**
 * Determines if the selection was on the dropdown affordance and, if so,
 * opens the drop down menu (populated using the same id as this item...
 * 
 * @param event
 * 		The SWT.Selection event to be tested
 * 
 * @return true iff a drop down menu was opened
 */
private boolean openDropDownMenu(Event event) {
	Widget item = event.widget;
	if (item != null) {
		int style = item.getStyle();
		if ((style & SWT.DROP_DOWN) != 0) {
			if (event.detail == 4) { // on drop-down button
				ToolItem ti = (ToolItem) item;

				final MenuManager menuManager = new MenuManager();
				Menu menu = menuManager.createContextMenu(ti.getParent());
				if (workbenchHelpSystem != null) {
					workbenchHelpSystem.setHelp(menu, helpContextId);
				}
				menuManager.addMenuListener(new IMenuListener() {
					public void menuAboutToShow(IMenuManager manager) {
						String id = getId();
						if (dropDownMenuOverride != null) {
							id = dropDownMenuOverride;
						}
						menuService.populateContributionManager(
								menuManager, "menu:" + id); //$NON-NLS-1$
					}
				});

				// position the menu below the drop down item
				Point point = ti.getParent().toDisplay(
						new Point(event.x, event.y));
				menu.setLocation(point.x, point.y); // waiting for SWT
				// 0.42
				menu.setVisible(true);
				return true; // we don't fire the action
			}
		}
	}

	return false;
}

메뉴를 만들기 위해 임시로 메뉴매니저를 사용하고, 플랫폼에 메뉴 서비스를 통해 기여시키지만, 릴리즈 및 디스포즈가 없습니다. menuManager가 로컬 변수 이므로, 집단 고립되서 GC에 수집될 것 같지만, 실제로는 그렇지 않습니다. 왜냐하면, 메뉴서비스와 이미 연결되어 버렸기 때문이지요.

사소한 릭일 것 같지만, CommandContributionItem은 핸들러 서비스, 커맨드 서비스와 결합할 뿐만 아니라 상황에 따라서는, 에디터의 셀렉션, 에디터 등과 같은 매우 비싼 객체를 참조하고 있는 경우도 심심찮게 발생합니다.

https://bugs.eclipse.org/bugs/show_bug.cgi?id=336584

Posted by 지이이이율
,