특집기사:EMF 튜토리얼 1부

위클립스
이동: 둘러보기, 찾기
Article.png 특집기사 정보
Jeeeyul.jpg
저자 이지율, 토마토 시스템 선임 연구원

이 문서는 예제 프로젝트를 통해 EMF의 개괄적인 개념들을 익히고, 모델의 설계, 생성, 조작, 저장및 로드, 모델 변경 감시 방법등을 소개한다.

목차

[편집] EMF란 무엇인가

EMF 프로젝트는 구조적인 데이터 모델에 기반한 애플리케이션을 위한 모델링 프레임워크이자 코드 생성기술이다.

실용 기술적인 측면에서 EMF는

  1. XMI[1]에 기술된 모델 명세를 바탕으로 해당 명세를 구현하는 Java 클래스들을 생성하는 도구를 제공하며,
  2. 생성된 코드가 작동하는데 필요한 런타임을 지원한다.
  3. 이러한 런타임 지원은 커맨드 기반의 기본 모델 편집기, 모델의 UI노출등을 돕는 어댑터 클래스들을 포함한다.


EMF가 제공하는 도구:


생성되는 코드:


EMF가 제공하는 런타임 API:

EMF의 작업 흐름은 매우 현실적며 실천적이다. 간단하게 말하면 UML 클래스 다이어그램을 정의하면, Java 코드를 즉시 생성할 수 있다. 추상적이거나 뜬 구름과 같은 MDA가 아니며, 실제로 유용한 모델 구현을 즉시 얻을 수 있다.

모델이 파일로 저장되고 UI를 통해 편집되는 애플리케이션을 개발하는 중이라면, EMF도입을 강력하게 권장한다.

[편집] 튜토리얼 개요

이 튜토리얼을 진행하면서 간단한 주소록 자바 애플리케이션을 만들어 볼 것이다. 이 주소록의 모델은 다음과 같다:

Emf-tutorial-model-overview.png

GroupEntry들로 구성되며, 각 자식 Entry들은 Groupparent로 역참조 한다. EntryGroupPerson으로 확장되며, Person은 이메일, 홈페이지 주소 그리고 전화번호 속성을 갖는다.

자 이제 위와 같은 모델을 선언하고 코드를 생성해 보자. 절차는 다음과 같다:

  1. 모델들을 담을 새 플러그인 프로젝트를 하나 만든다.
  2. Ecore 파일을 작성하여 모델을 정의한다.
  3. Gen Model 파일을 작성하여 소스 코드 생성 방법을 구체화 한다.
  4. 소스코드를 생성한다.

[편집] 모델링

모델링을 진행하려면 이클립스 모델링 패키지를 설치할 것을 권장한다. 다른 이클립스에서 수동으로 EMF SDK를 설치하는 것은 본 튜토리얼에서 다루지 않는다.

[편집] Ecore 파일 만들기

사실 EMF는 Ecore 외에도 다양한 포맷에 담긴 모델 명세를 바탕으로 코드를 생성할 수 있지만, 본 튜토리얼에서는 표준 입력인 Ecore 파일을 기준으로 설명한다. 먼저 모델링을 하기 위해 Ecore 파일을 만들자.

  1. 새 플러그인 프로젝트를 하나 만들고, 이름을 kr.or.eclipse.addressbook 으로 한다.
  2. 프로젝트를 우 클릭하고 New > Folder를 선택하고 이름이 model인 폴더를 만든다.
  3. 만들어진 폴더를 우 클릭하고, New > Other 를 선택한 뒤 Eclipse Modeling Framework 항목 아래의 Ecore Model을 선택하고, Next를 클릭한다.
  4. File name 필드에 addressbook.ecore를 입력하고 Finish를 누른다.

Ecore-creation-1.png

Ecore-creation-2.png

파일을 만들고 나면 다음과 같은 편집기를 볼 수 있을 것이다.

Empty-ecore.png

[편집] EPackage

패키지란 모델의 타입들이 속한 영역을 추상화 하는 역할을 한다. 자바의 세계에서 클래스이름이 동일하더라도 패키지가 다르면 공존할 수 있는 것 처럼, EMF에서도 모든 요소들은 고유의 패키지 내에 존재해야 한다. 편집기상에서 패키지를 더블클릭하면(위쪽 에디터에 선택되어 있는 부분이 패키지이다), 속성 뷰가 바로 열린다. 속성뷰에는 다음과 같은 항목들이 나열되어 있다:

속성 설명
Name 패키지의 이름
Ns Prefix 네임스페이스 프리픽스. XML로 저장될 때, 콜론 앞부분에 쓰일 키워드. ex) <ab:group />에서 ab.
Ns URI 패키지를 구분하기 위한 고유 URI. 이클립스내에는 수많은 EMF 모델들이 공존한다. 특정 모델을 로드할 때 어떤 패키지나 팩토리를 사용할 것인지 구분하기 위해 이 주소가 사용된다.

XSD를 이용한 XML 스키마를 디자인 해 본적이 있다면, 이부분은 이해가 쉬울 것이다.

패키지를 선택하고 속성창에서 다음과 같이 내용을 입력하자:

Emf-tutorial-create-package.png

[편집] EClass

패키지를 만들었으면, 이제 클래스들을 만들어 보자. 패키지를 우 클릭하고 New Child > EClass 를 차례로 선택하자. 만들어진 EClass를 선택하면 속성창에 다음과 같은 항목들이 나타난다.

속성 설명
Abstract 이 클래스 정의(EClass)가 추상 클래스를 정의하는 것인지 여부.
ESuper Types 네임스페이스 프리픽스. XML로 저장될 때, 콜론 앞부분에 쓰일 키워드. ex) <ab:group />에서 ab.
Instance Type Name 자바로 생성된 클래스의 타입명이 이름과 다른 경우에만 사용한다. 일반적으로 비워둔다.
Interface 이 클래스 정의가 인터페이스를 정의하는 것인지 여부.
Name 클래스의 이름.

다른 속성들은 단순히 참고로만 하고, Name 속성만을 편집하여 Entry, Person, Group, 3개의 EClass를 만들어 보자.

그리고, PersonGroup의 부모 클래스로 Entry를 지정하자:

Emf-tutorial-set-super-type.png

[편집] Feature

피쳐란 특정 EClass가 다른 EClass들과 구분될 수 있는 특징을 추상화 하는 단위이다. 방금 우리가 추가한 3개의 클래스에 개성을 부여하려면 이러한 피쳐들을 추가해야 한다.

피쳐는 속성(EAttribute)과 관계(EReference)로 나뉘어진다.

[편집] EAttribute

예를 들어 앞서 본 클래스 다이어그램에서 PersonemailphoneNumber, 그리고 web이라는 EString을 속성으로 갖고 있는데, 이 경우 Person EClass는 3개의 속성 피쳐를 갖고 있다고 할 수 있다.

일반적으로 속성으로는 프리미티브 타입의 단순한 데이터 값만 가질 수 있으며, EString, EBoolean, EInteger등, 자바의 기본 프리미티브 타입에 대한 타입들이 미리 공급된다.

기구현된 다른 클래스를 데이터 타입으로 선언하여 사용할 수 있으나, 본 튜토리얼에서는 다루지 않는다.

Entry EClass를 우클릭하고 New Child > EAttribute를 차례로 선택 한 뒤, 속성창에서 다음과 같이 입력한다:

Emf-tutorial-create-attribute.png

마찬가지로 Person을 선택하고 다음 EAttribute 들을 추가하라, 타입은 모두 EString이다:

[편집] EReference

속성 피쳐들을 모두 추가했으면, 이제 모델의 위상을 잡기 위해 관계들을 추가해야 한다.

관계는 그 유형에 따라 크게 단순 참조와, 포함 참조로 나눌 수 있다.

[편집] 단순 참조

단순 참조는 독립적인 두 인스턴스들이 참조를 갖는 경우를 말한다. 이 경우, 관계가 파기되거나 삭제되더라도 두 인스턴스는 독립적으로 존재할 수 있다.

[편집] 포함 관계(Containment Reference)

포함관계란 GroupEntry에 대하여 포함관계를 갖는다고할 때, EntryGroup의 부속물로 취급되는 것을 말한다. 이 경우 Group이 삭제되거나 관계가 제거되면, 관계를 맺은 모든 Entry도 삭제된다. UML의 경우, 이러한 관계를 표기할 때, 속이 채워진 다이아몬드를 부모쪽에 표기한다.

  1. 이제 Group EClass를 선택하고, 우클릭하여 New Child > EReference를 차례로 선택하자.
  2. 먼저 Nameentries로 지정하자.
  3. Group은 0에서 임의의 갯수의 Entry들을 가질수 있으므로, Upper Bound-1로 지정하자. (-1은 무제한을 의미한다)
  4. 그리고 타입을 Entry로 설정 한 뒤,
  5. Containment값을 true로 설정하여 이 관계가 포함관계임을 지정하자.

Emf-tutorial-create-reference.png

[편집] 부모 관계(Container Reference)

포함 관계가 성립하면, 반대로 피포함 관계가 설립될 수 있다. 우리의 UML에서 Entryparent라는 관계 피쳐를 갖는데, 이가 바로 피 포함 관계를 담는 피쳐이며, 이를 부모 관계라고 한다.

포함 관계와 단순 참조 관계가 헷갈린다면, 이들이 저장되는 형태를 통해 쉽게 이해할 수 있을 것이다:

단순 참조의 예:

<entry id="entry1" name="entry1" />
<entry id="entry2" name="entry2" />
 
<group id="group1"
       entries="entry1 entry2"> // id만 참조한다.
</group>

포함 관계의 예:

<group id="group1">
   <entry id="entry1" name="entry1" />
   <entry id="entry2" name="entry2" />
</group>

실제 부모 관계의 추가는 다음 절에서 보충 설명 후에 진행한다.

[편집] 역참조 관계

부자지간이라는 한 관계를 떠올려 보자. 이는 하나의 관계지만, 아버지의 입장에서는 이 관계 끝에 존재하는 것이 아들이고, 아들의 입장에서는 이 관계 끝에 존재하는 것이 아버지이다. 즉 아버지와 자식 입장에서 이 관계는 두개의 관계 피쳐로 정의되어야 한다. 아버지가 다른 객체를 아들로 보았을 경우, 아들 객체는 아버지 객체를 반드시 아버지로 보아야 하며, 어느 한쪽만 아버지나 아들로 독립적으로 관계를 맺을 수 없다. 이러한 관계를 상호 역참조 관계라고 한다.

우리의 UML에서는 Groupentries라는 이름으로 복수의 Entry 인스턴스에 대해 포함 참조 관계(Containment Reference)를 갖는다. 이와 동시에 각각의 Entry들은 parent라는 피쳐로 자기자신이 포함된 부모를 가리킨다.

이런 상태에서 아래의 코드가 수행되는 경우를 생각해 보자:

  1. Entry entry1 = ...;
  2. Group groupA = ...;
  3.  
  4. groupA.getEntries().add(entry1);
  5.  
  6. // entry1.setParent(..)를 호출한 적이 없지만, 이 값은 참이 된다.
  7. boolean test = (entry1.getParent() == groupA);

4위의 코드에서 groupAentry1이 추가되는 순간, entry1parentgroupA가 될 것이다. 이렇게 특정한 관계의 설립에 의해 자동으로 설립되는 관계를 역참조(Opposite Reference) 관계라고 한다.

이러한 동작은 대칭성이 보장되므로, 반대로 다음과 같은 코드도 가능하다:

  1. Entry entry1 = ...;
  2. Group groupA = ...;
  3.  
  4. entry1.setParent(groupA);
  5.  
  6. // 이 값은 참이 된다.
  7. boolean test = (groupA.getEntries().contains(entry1));

Groupentries 피쳐와, Entryparent 피쳐는 서로 역참조 관계가 된다.

이제 parent 피쳐를 추가해 보자:

  1. Entry EClass를 선택하고, 우클릭 한 후 New Child > Reference 를 차례로 선택한다.
  2. Name 항목에 parent라고 기재한다.
  3. EType 항목에서 Group을 선택한다.
  4. Container 항목을 true로 지정하여, 부모 관계임을 지정한다.
  5. EOpposite 항목에서 entries:Entry를 선택하여, 역참조 관계를 설정한다.

Emf-tutorial-create-container-reference.png

자 이제 모델 정의가 끝났다. 여기까지 오는데 뭔가 문제가 있었거나, 직접 해 보는게 귀찮다면 소스를 다운로드 받아 임포트 한 후, 문제를 살펴보자.

여기까지 잘 진행되었다면, 프로젝트 레이아웃은 다음과 같아야 한다.

[편집] 코드 생성

[편집] 코드 생성 모델

모델에 대한 정의는 끝났지만, 자바 소스코드를 어떤식으로 생성할 것인지 추가적인 정보가 더 필요하다. 이러한 정보들은 genmodel 파일 안에 기술 된다.

  1. addressbook.ecore 파일을 우클릭 하고, New > Others...를 선택한다.
  2. Eclipse Modeling Framework 항목 밑에서 EMF Generator Model을 선택하고 Next를 누른다.
  3. 이미 File name 필드에 addressbook.genmodel이라고 씌여져 있겠지만, 그렇지 않은 경우 입력하고, Next를 누른다.
  4. 입력 모델로 Ecore model을 선택하고 Next를 누른다.
  5. Load 버튼을 눌러 입력 모델을 로드하고 Next를 누른다.
  6. Finish를 누른다.

그러면 다음과 같은 에디터를 볼 수 있다:

Genmodel-editor.png

겉보기에는 Ecore 에디터와 유사한 것 같지만, 편집할 수 있는 속성들이 전혀 다르다.

Addressbook EPackage를 선택하고, 속성 뷰에서 All 항목 밑의 Base Package값에 kr.or.eclipse라고 입력하자.

이렇게하면 생성될 패키지가 addressbook 대신 kr.or.eclipse.addressbook이 된다. 나머지 속성들도 유심히 살펴볼 것을 권장한다. 이들을 살펴보면 이미 모델을 정의했음에도 왜 따로 코드 생성모델을 다시 만들어야 하는지 이해될 것이다. 그 내용들을 이 튜토리얼에서 다루지는 않는다.

[편집] 코드 생성기 실행

이제 코드 생성 모델 편집기에서 Addressbook EPackage를 선택하고, 우클릭한 뒤 Generate Model Code를 선택하자.

코드 생성이 완료되면, 다음과 같은 형태로 코드가 생성될 것이다:

Emf-tutorial-generated.png

생성된 코드들을 살펴보자, 뭔가 문제가 있었거나 직접하기 귀찮은 사람들은 이곳에서 생성된 코드들을 둘러볼 수 있다.

또한 이 단계 까지의 소스코드를 다운로드 할 수 있다.

[편집] Eclipse에 모델 출판

생성된 코드들을 좀 유심히 살펴보았다면, 자바 소스 이외에도 몇 가지가 더 변경되었다는 사실을 알 수 있는데, 그중 제일 중요한 것은 plugin.xml 파일의 내용이다.

<?xml version="1.0" encoding="UTF-8"?>
<?eclipse version="3.0"?>
<plugin>
   <extension point="org.eclipse.emf.ecore.generated_package">
      <!-- @generated addressbook -->
      <package
            uri="http://eclipse.or.kr/adressbook"
            class="kr.or.eclipse.addressbook.AddressbookPackage"
            genModel="model/addressbook.genmodel"/>
   </extension>
</plugin>

org.eclipse.emf.ecore.generated_package 확장이 하나 등록된 것을 볼 수 있다.

이는 EMF 인스턴스 파일 중, 네임스페이스가 http://eclipse.or.kr/adressbook인 컨텐트를 만나면, 이것이 AddressbookPackage 소속의 모델이라는 사실을 이클립스 워크벤치에 알려주는 역할을 한다.

다음으로 눈여겨 볼 파일은 MANIFEST.MF 파일이다. 이파일을 열어 런타임 탭을 눌러 보면, Exported Packages 섹션에 다음과 같은 패키지들이 나열됨을 알 수 있다:

이는 이 플러그인디펜던시로 갖는 다른 플러그인들이 생성된 모델 API를 사용할 수 있도록 허가하는 역할을 한다.

[편집] 생성된 코드 사용해 보기

이제 모델을 사용해 보기 위해 새로운 플러그인 프로젝트를 하나 더 만들자.

  1. 새 플러그인 프로젝트 kr.or.eclipse.addressbook.app를 만든다.
  2. META-INF/MANIFEST.MF 파일을 더블 클릭하여 Dependencies 탭으로 들어간다.
  3. Add... 버튼을 눌러 kr.or.eclipse.addressbook 을 추가한다.

[편집] 모델 생성 및 다루기

kr.or.eclipse.addressbook.app.test 패키지를 만들고, 그 안에 BasicTest 클래스를 만든 다음, 다음과 같이 입력하자:

  1. package kr.or.eclipse.addressbook.app.test;
  2.  
  3. import kr.or.eclipse.addressbook.AddressbookFactory;
  4. import kr.or.eclipse.addressbook.Group;
  5. import kr.or.eclipse.addressbook.Person;
  6.  
  7. public class BasicTest {
  8.    public static void main(String[] args) {
  9.       Group family = AddressbookFactory.eINSTANCE.createGroup();
  10.       family.setName("가족");
  11.  
  12.       Person father = AddressbookFactory.eINSTANCE.createPerson();
  13.       father.setName("아빠");
  14.       father.setEmail("father@blahblah.net");
  15.       father.setPhoneNumber("012-3456-7890");
  16.  
  17.       family.getEntries().add(father);
  18.  
  19.       System.out.println(father.getParent().getName());
  20.    }
  21. }

다른 점들은 직관적이니 부가적인 설명이 거의 필요 없다. 다만 912에서 모델을 생성할 때, 팩토리를 이용하는 것을 눈여겨 보자.

[편집] 모델 저장 및 로드

EMF에서 모델을 저장하는 기본적인 절차는 다음과 같다.

  1. 리소스를 만든다.
  2. 리소스의 컨텐츠로 모델을 넣는다.
  3. 리소스의 저장 API를 이용한다.

절차에 따르면 먼저 리소스를 만들어야 하는데, 리소스에는 여러 종류가 있다. 기구현되어 공급되는 리소스는 다음과 같다:

리소스의 종류에 따라, 저장하는 형태가 결정된다.

리소스를 확보하는 방법은 다양하다. 예컨데 직접 생성자를 호출하여 원하는 리소스를 만들어도 된다. 하지만 본 문서에서는 표준 절차만 언급하도록 하겠다.


같은 패키지에 SaveTest 클래스를 추가하고 다음과 같이 입력한다:

  1. package kr.or.eclipse.addressbook.app.test;
  2.  
  3. import java.io.IOException;
  4. import java.util.HashMap;
  5.  
  6. import kr.or.eclipse.addressbook.AddressbookFactory;
  7. import kr.or.eclipse.addressbook.Group;
  8. import kr.or.eclipse.addressbook.Person;
  9.  
  10. import org.eclipse.emf.common.util.URI;
  11. import org.eclipse.emf.ecore.resource.Resource;
  12. import org.eclipse.emf.ecore.resource.ResourceSet;
  13. import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;
  14. import org.eclipse.emf.ecore.xmi.XMIResource;
  15. import org.eclipse.emf.ecore.xmi.impl.XMIResourceFactoryImpl;
  16.  
  17. public class SaveTest {
  18.    public static void main(String[] args) throws IOException {
  19.       Group family = AddressbookFactory.eINSTANCE.createGroup();
  20.       family.setName("가족");
  21.  
  22.       Person father = AddressbookFactory.eINSTANCE.createPerson();
  23.       father.setName("아빠");
  24.       father.setEmail("father@blahblah.net");
  25.       father.setPhoneNumber("012-3456-7890");
  26.  
  27.       family.getEntries().add(father);
  28.  
  29.       ResourceSet set = new ResourceSetImpl();
  30.       set.getResourceFactoryRegistry()
  31.             .getExtensionToFactoryMap()
  32.             .put(Resource.Factory.Registry.DEFAULT_EXTENSION, new XMIResourceFactoryImpl());
  33.  
  34.       XMIResource resource = (XMIResource) set.createResource(URI.createFileURI("C:/Users/Jeeeyul/Desktop/test.xml"));
  35.       resource.setEncoding("ms949");
  36.  
  37.       resource.getContents().add(family);
  38.  
  39.       resource.save(System.out, new HashMap<Object, Object>());
  40.    }
  41. }

19에서 27까지는 앞선 예제와 동일하다. 붙여 넣어도 무방하다.

[편집] 리소스 셋

29에서 ResourceSet이 등장한다. 리소스 셋이란 말 그대로 리소스들의 집합이다. 오늘날의 구조적인 문서들은 대게 다른 문서들과 유기적인 관계를 맺기 마련이다. HTML 문서를 생각하면 쉽다. 문서는 다른 문서로 링크를 갖거나 CSS 스타일 파일, 이미지를 끼워넣는 등 수많은 리소스 파일들이 집합되어 실제 문서가 완성된다. 따라서 단일 리소스 개념만으로는 완전한 문서모델을 원하는데로 저장하거나 로드하게 하는 것이 어렵다.

리소스 셋은 이렇게 복수의 리소스들로 부터 문서를 저장하거나 보존하는 API및 기반 시스템을 제공한다. 리소스셋의 주요 역할은 다음과 같다:

30에서 32에서는 어떤 확장자이건 기본적으로 XMIResource를 만들도록 XMIResourceFactoryImpl를 등록한다. 딱히 다른 이유는 없고 XMI가 메타데이터 교환용 표준이기 때문이다.[1]

34에서 XMIResource로 캐스팅 했는데, 우리는 앞서 XMI 리소스만을 이용하도록 팩토리를 지정해 주었기 때문에, 가능하다. 굳이 캐스팅 한 이유는 35 인코딩을 지정하기 위해서이다. ms949를 사용한 이유는 대게의 독자는 한글이 기본 로케일로 지정된 윈도우를 사용할 것이기 때문에, 콘솔에서 깨져서 출력되는 것을 방지하기 위함이다. URI 로 바탕화면의 test.xml로 지정했는데, 본인의 주소로 수정해야 한다.

37에서 모델을 리소스에 넣게 된다.

39에서 리소스의 API를 이용해 모델을 저장한다. 실제 파일로 저장하는 것은 아니고 출력 스트림으로 System.out을 지정해 주었기 때문에 콘솔로 출력될 것이다.

일단 이 시점에서 위의 코드를 실행 해 보자. 그러면 다음과 같은 결과가 콘솔에 출력될 것이다:

<?xml version="1.0" encoding="ms949"?>
<ab:Group xmi:version="2.0" xmlns:xmi="http://www.omg.org/XMI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ab="http://eclipse.or.kr/adressbook" name="가족">
  <entries xsi:type="ab:Person" name="아빠" email="father@blahblah.net" phoneNumber="012-3456-7890"/>
</ab:Group>

이제 System.out부분을 지우고 다시 실행해보면 바탕화면에 test.xml 파일이 생성된 것을 확인할 수 있을 것이다.

뭔가 제대로 진행되지 않았거나 직접 해보기 귀찮은 경우 여기를 클릭하여 다운로드 받거나 https://github.com/jeeeyul/emf-tutorial/tree/SaveTest에서 소스코드를 둘러 볼 수 있다.

[편집] URI

34에서 URI.createFileURI(String)를 이용했는데, 이 URI는 Java에 포함된 URI가 아님을 유의하자. EMF에서 널리 사용되는 URI 종류는 다음과 같다:

유형 설명 생성방법
File 데스크 탑 내에 존재하는 파일에 대한 URI URI.createFileURI(...)
플랫폼 리소스 실행중인 이클립스의 워크스페이스 내에 있는 파일에 대한 URI URI.createPlatformResourceURI(...)
플랫폼 플러그인 리소스 실행중인 이클립스에 설치된 플러그인의 특정 리소스에 대한 URI URI.createPlatformPluginURI(...)

본 튜토리얼은 이클립스 플러그인 기준으로 설명하지 않고, 평범한 자바 애플리케이션 개발로 진행 될 것이기 때문에 File URI를 사용하였다.

[편집] Resource

앞서 언급했다시피 리소스의 종류에는 여러가지가 있고, 추가적으로 직접 개발 할 수 도 있지만 공통 인터페이스에 대해서만 몇가지 언급한다.

리소스는 일반적으로 URI를 소유하며, 이를 통해 어디로부터 로드되고 어디로 저장할 것인지를 특정한다. 하지만 이 값이 null 일수도 있다. 그런경우, saveload 메서드 호출시 반드시 입/출력 스트림을 인자로 주어야 한다. URI를 갖고 있는 경우라면, 단순히 save(Map options) 또는 load(Map options)만 호출하면 된다. 세이브/로드는 단순히 저장만을 하는 것이 아니라 URI에 맞추어 더 적합하게 작동한다. 예를 들어 워크스페이스 내 파일에 대한 URI(플랫폼 리소스 URI)인 경우에는 파일 저장시 자동으로 워크스페이스 싱크가 이뤄진다.

세이브/로드시 옵션은 맵 형태로 전달되는데 사용될 수 있는 키들은 Resource 클래스에 OPTION_XXX 형태의 상수로 선언되어 있다. 특정 유형의 리소스의 경우, 확장된 인터페이스에서 추가 옵션을 확인할 수 있다.

성능과 관련하여 거의 반드시 사용해야 하는 옵션들이 있는데 조리법:EMF 퍼포먼스 팁 문서에서 확인 할 수 있다.

[편집] 모델 로드

이제 모델을 로드해 보자. test 패키지에 LoadTest 클래스를 추가하고 다음과 같이 입력한다:

  1. package kr.or.eclipse.addressbook.app.test;
  2.  
  3. import java.io.IOException;
  4. import java.util.HashMap;
  5.  
  6. import kr.or.eclipse.addressbook.AddressbookPackage;
  7. import kr.or.eclipse.addressbook.Group;
  8.  
  9. import org.eclipse.emf.common.util.URI;
  10. import org.eclipse.emf.ecore.resource.Resource;
  11. import org.eclipse.emf.ecore.resource.ResourceSet;
  12. import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;
  13. import org.eclipse.emf.ecore.xmi.XMIResource;
  14. import org.eclipse.emf.ecore.xmi.impl.XMIResourceFactoryImpl;
  15.  
  16. public class LoadTest {
  17.    public static void main(String[] args) throws IOException {
  18.       ResourceSet set = new ResourceSetImpl();
  19.       set.getPackageRegistry().put(AddressbookPackage.eNS_URI, AddressbookPackage.eINSTANCE);
  20.  
  21.       set.getResourceFactoryRegistry()
  22.             .getExtensionToFactoryMap()
  23.             .put(Resource.Factory.Registry.DEFAULT_EXTENSION, new XMIResourceFactoryImpl());
  24.  
  25.       Resource resource = set.createResource(URI.createFileURI("C:/Users/Jeeeyul/Desktop/test.xml"));
  26.  
  27.       resource.load(new HashMap<Object, Object>());
  28.  
  29.       Group group = (Group) resource.getContents().get(0);
  30.       System.out.println(group);
  31.    }
  32. }

세이브시와는 좀 다른 점이 있다. 19 부분은 세이브시에는 없었던 내용이다. 저장된 XML의 내용중 xmlns:ab="http://eclipse.or.kr/adressbook" 로 네임스페이스 정보가 기재되는 부분이 있는데.

EMF로선 http://eclipse.or.kr/adressbook가 어떤 패키지에 대한 네임스페이인지 알 도리가 없다. 따라서 19에서 처럼 NS URI에 대한 패키지를 알려줘야 한다.

참고로 한 번이라도 패키지내의 모델이나 API가 사용되면 자동적으로 글로벌 등록된다. 혹은 명시적으로 다음과 같이 패키지를 초기화하고 등록하는 방법도 있다:

AddressbookPackageImpl.init();

만약 여러분들이 이클립스 플러그인을 만드는 경우라면, 이러한 절차가 필요없다. 이클립스가 시작되면서 저절로 확장점을 통해 패키지들이 등록되기 때문이다.

실행 해 보면 모델이 잘 로드 되는 것을 알 수 있다.

진행에 문제가 있거나 직접해 보는 것이 귀찮다면, 여기까지의 진행 상황을 다운로드 받거나 소스를 탐색 해 볼 수 있다.

[편집] 모델 변경 감시

일반적으로 모델은 상태 보존, 직렬화 외에도 모델 상태 변경 통지의 임무를 갖는다. EMF가 생성한 모델에도 물론 그러한 기능이 갖춰져 있다.

그러나 일반적인 addXXXListener(...)의 형태와는 다소 다르고, 이를 학습하기전에 어댑터 패턴의 기본 개념을 이해해야 한다.

[편집] 어댑터

EMF서는 리스너라는 용어대신 어댑터라는 용어를 사용한다.

모델의 상태 변경에 대응하는 코드들을 특정 영역에 대하여 모델의 기능이 확장된 것으로 보기 때문이다.

즉, 모델과 특정 도메인 영역 사이를 이어주는 모든 것을 어댑터로 보는 것이고, 이는 리스너 보다 좀 더 일반화된 개념이라고 여기면 된다.

대게의 어댑터는 모델의 상태 변경에 따라 대응하는 형태지만 그렇지 않은 종류들도 있다는 것만 알아두자. 이에 대한 내용은 특집기사:EMF 튜토리얼 2부에서 좀더 자세히 기술 된다.

현시점에서 알아두어야 할 내용들은 다음과 같다.

모든 모델 인스턴스들은 eAdapters() 라는 메소드를 통해 현재 보유한 어댑터 목록들을 얻을 수 있으며, 이 반환 값(EList 형태의 컬렉션)에 어댑터를 직접 추가할 수도 있다.

어댑터의 인터페이스는 다음과 같다:

public interface Adapter{
  // 모델 상태 변경이 일어나면 이 메소드로 통지 받게 된다.
  // 본 튜토리얼 시점에서는 이 메소드가 가장 중요하며 나머지는 이해하지 않아도 좋다.
  void notifyChanged(Notification notification);
 
  // 어댑터를 소유한 모델을 리턴한다. 그러나 공유 어댑터인 경우 이 값은 별 의미가 없다.
  Notifier getTarget();
 
  // 어댑터를 소유할 모델을 지정한다.
  void setTarget(Notifier newTarget);
 
  // 이 어댑터가 주어진 모델을 지원하는지 여부를 반환한다.
  boolean isAdapterForType(Object type);
}

void notifyChanged(Notification notification) 메소드를 제외하고는 동적으로 상황에 따라 어댑터가 결정되는 유연한 프로그래밍을 돕기 위한 API로 현시점에서는 중요치 않다, 차후 자세히 다룰 것이다.

[편집] 모델 변경 감시를 위한 어댑터 부착

test 패키지에 NotificationTest 클래스를 새로 만들과 아래와 같이 입력하자:

  1. package kr.or.eclipse.addressbook.app.test;
  2.  
  3. import org.eclipse.emf.common.notify.Notification;
  4. import org.eclipse.emf.common.notify.impl.AdapterImpl;
  5.  
  6. import kr.or.eclipse.addressbook.AddressbookFactory;
  7. import kr.or.eclipse.addressbook.Person;
  8.  
  9. public class NotificationTest {
  10.    public static void main(String[] args) {
  11.       Person person = AddressbookFactory.eINSTANCE.createPerson();
  12.       AdapterImpl adapter = new AdapterImpl() {
  13.          @Override
  14.          public void notifyChanged(Notification msg) {
  15.             System.out.println(msg.isTouch());
  16.             System.out.println(msg);
  17.          }
  18.       };
  19.       person.eAdapters().add(adapter);
  20.  
  21.       person.setName("이클립스");
  22.       person.setWeb("http://eclipse.org");
  23.  
  24.       person.eAdapters().remove(adapter);
  25.    }
  26. }

12에서 어댑터의 기본 구현인 AdapterImpl을 상속받아 14notifyChanged(Notification msg)를 오버라이드 하였다. 모델 변경에 대한 자세한 정보는 Notification 객체에 담겨 전달된다. 이 클래스의 소스를 꼼꼼히 뜯어보라.

15Notification#isTouch()는 모델이 변경되기는 했지만, 실제 상태가 바뀌지는 않은 경우 true를 리턴한다. 말 그대로, 툭 건드리기만 한 변경인지를 나타낸다. 예를들어 어댑터가 추가되거나 삭제한 경우, 값이 지정되기는 하였으나 이전과 동일한 값인 경우등이 이에 해당한다.

이코드를 실행해 보면 다음과 같은 결과를 볼 수 있다:

false
org.eclipse.emf.ecore.impl.ENotificationImpl@7bfbfeae (eventType: SET, ...후략
false
org.eclipse.emf.ecore.impl.ENotificationImpl@656d2378 (eventType: SET, ...후략
true
org.eclipse.emf.ecore.impl.MinimalEObjectImpl$1ArrayDelegatingAdapterLi...후략

실제로 모델의 상태가 변경된 것은 2122 뿐이지만 3개의 메시지가 전달된다. 마지막 메시지는 어댑터가 제거되기 직전 어댑터 제거사실을 알리는 메시지이다. 만약 모델에 다른 어댑터가 미리 존재했었다면, 새로운 어댑터가 부착될 때에도 메시지를 수신하게 된다.

일반적인 MVC 패턴에서 모델의 실제 상태가 변경된 경우에만 뷰를 업데이트 하므로, isTouch()를 이용하여 불필요한 뷰 갱신을 억제해야 한다.

[편집] Notification

주로 사용되는 Notification 객체의 API들을 알아보자.

메소드 용도
getFeature() 어느 피쳐에 대한 변경인지를 리턴한다.
getOldValue() 변경된 피쳐의 이전 값을 리턴한다.
getNewValue() 변경된 피쳐의 새로운 값을 리턴한다.
getNotifier() 메시지의 발송자(상태가 변경된 모델)를 리턴한다.

예를 들어 웹 주소가 변경된 경우에만 반응하는 어댑터를 작성하려면 다음과 같이 하면 된다.

package kr.or.eclipse.addressbook.app.test;
 
import org.eclipse.emf.common.notify.Notification;
import org.eclipse.emf.common.notify.impl.AdapterImpl;
 
import kr.or.eclipse.addressbook.AddressbookFactory;
import kr.or.eclipse.addressbook.AddressbookPackage;
import kr.or.eclipse.addressbook.Person;
 
public class NotificationTest2 {
   public static void main(String[] args) {
      Person person = AddressbookFactory.eINSTANCE.createPerson();
      AdapterImpl adapter = new AdapterImpl() {
         @Override
         public void notifyChanged(Notification msg) {
            if (msg.getFeature() == AddressbookPackage.eINSTANCE.getPerson_Web()) {
               System.out.println("웹 주소가 변경되었습니다.");
            }
         }
      };
      person.eAdapters().add(adapter);
 
      person.setName("이클립스");
      person.setWeb("http://eclipse.org");
 
      person.eAdapters().remove(adapter);
   }
}

진행에 문제가 있거나 직접해 보는 것이 귀찮다면, 여기까지의 진행 상황을 다운로드 받거나 소스를 탐색 해 볼 수 있다.

[편집] 정리

우리는 이문서를 통해:

  1. Ecore로 모델을 정의하고,
  2. 모델 코드를 생성하고
  3. 생성된 코드를 이용하여 모델을 조작하고
  4. 모델을 직렬화하거나 로드하고
  5. 모델 변경을 감시하는 방법을 배웠다.

특집기사:EMF 튜토리얼 2부에서 EMF가 UI를 어떻게 지원하는지를 중점적으로 EMF의 고급 기능등을 더 둘러볼 예정이다.

[편집] 참조

  1. 1.0 1.1 XMI: http://www.omg.org/spec/XMI/

이 기사에 대한 의견은 토론 페이지를 통해 나눌 수 있습니다.

개인 도구
이름공간
변수
행위
포탈
탐색
도움
도구모음