특집기사:GEF의 이해 1부

위클립스
이동: 둘러보기, 찾기
Article.png 특집기사 정보
Jeeeyul.jpg
원문 보기
저자 Régis Lemaigre
역자 이지율

최초 Régis Lemaigre에 의해 씌여진 뒤, 이클립스 커뮤니티에 의해 다듬어지고, 이지율에 의해 번역 및 첨삭 된 비공식 GEF 문서이다. 이 문서는 GEF의 개괄적 이해 및, 모델로 부터 뷰가 구성되는 과정까지를 다루며, 편집 기능에 대해서는 다루지 않는다. 편집기능은 특집기사:GEF의 이해 2부에서 다뤄질 예정이다.

목차

[편집] GEF가 해결해 주는 문제

GEFGraphical Editing Framework의 약자로, 이클립스에서 시각 기반 편집기를 만드는 일을 쉽게 해 주는 라이브러리이다.

[편집] 시각기반 편집의 보편적인 문제들

다음과 같은 요구사항을 바탕으로 애플리케이션을 개발해야 한다고 가정하자:

이렇게 GUI 인터페이스를 이용하여 사용자가 모델을 수정할 수 있는 방법을 제공하는 애플리케이션을 시각 기반 편집기라고 한다.

뷰, 모델, 그리고 그 사이에 존재하는 연결은 개략적으로 아래 그림과 같이 나타낼 수 있다. 앞으로 차차 더 구체적으로 설명하도록 노력할 것이다:

Gef desc im26.gif


이 문제에서 모델과 뷰 사이의 연결은 무엇이든 될 수 있다. 예컨데 같은 모델에 대한 두 상태 객체가 될 수 있다. 만약 모델 내부에 위치와 같은 시각적인 정보를 가지고 있지 않다면, 사용자가 그림을 드래그해서 옮기더라도, 이 정보는 모델에 반영되지 않을 것이다. 따라서 이러한 상태는 개별의 객체에 존재할 것이고 이런 경우 동일 상태의 모델에 대해 여러 표현이 동시에 존재할 수 있다.

시각기반 에디터는 아래와 같이 동작한다:

Gef desc im27.gif


하지만 이런 그림으로 명확한 설명을 하기에 문제가 너무 흐릿하다고 생각한다. GEF를 막 시작하는 사람들에게 명확한 개념을 전달하기 위해, 좀 일반적인 문제를 GEF로 어떻게 풀어나가는지 설명하겠다. 잘못된 생각일 수도 있으나, 나는 GEF가 좀 덜 일반적인 시각 편집 문제를 위해 생겨났다고 생각한다. 이 생각이 옳든 그르든, 구체적인 문제로 진행해서 명확하고 단순한 소개를 하는 것이 좋다고 믿는다.

[편집] 덜 일반적인 시각 기반 편집 문제

이 문제는 뷰와 모델이 특수한 제약이 있다는 것을 제외하고는 앞서 말한 문제와 동일하다:

각각의 모델들은, 그에대한 대한 뷰가 화면에 어떻게 표시될 것인가를 결정하는 모든 상태에 대한 데이터를 반드시 포함해야 한다.

따라서, 뷰는 모델의 상태에 따라 어떻게 표현될지 전체가 결정될 수 있어야 한다, 즉 뷰를 모델 상태에 대한 한 기능으로 취급할 수 있어야한다. 이를 그림으로 나타내면 다음과 같다:

Gef desc im28.gif

범용성을 약간 포기한 댓가로 애플리케이션의 아키텍쳐는 훨씬 간단해진다:

이제 뷰는 모델의 상태에의해서 완전히 정의된다, 따라서 사용자가 GUI를 통해 어떤 액션을 일으켜 뷰가 새 상태를 가지는 전 과정을 모델의 변경으로 나타낼 수 있다. 즉 사용자 액션은 모델 변경으로 번역 가능 해 진다. 이로 인해 우리는 깔끔한 분리를 이뤄낼 수 있다:

이것을 그림으로 나타내 보면:

Gef desc im29b.GIF


모델이 실제로 뷰를 업데이트하는 책임을 가진다는 뜻이 아니다, 이 그림은 개념적인 흐름을 보여준다. 몇몇 세부 그림은 여기서 누락되어 있다.

이를 구현하기 위해, 당신은 3개의 문제를 풀어야 한다:

  1. 에디터가 열렸을 때, 당신이 모델에 대해 정의해둔 뷰들이 저절로 빌드되고 표시되는 어떤 매커니즘을 구현해야한다.
  2. 모델의 어떤 상태가 변경되었을때 그와 연결된 뷰의 특성을 업데이트하는 매커니즘을 구현해야 한다.
  3. 사용자의 액션을 잡아내어 그것을 모델을 수정하는 변경 객체로 번역해야 한다.

GEF가 도와주는 것이 바로 이런 문제들이며, 다음 설명들을 통해 하나 하나 살펴보겠다.

[편집] 모델-뷰-컨트롤러 아키텍쳐

나는 당신이 무슨 생각을 하는지 안다: 당신은 이 섹션이 GEF 블랙 박스 내부의 일들이 어떻게 돌아가는지 알고 싶어 환장한 변태들을 위한 것이며, 건너 뛰어도 된다고 생각하고 있다. 아니면 다행이지만, 절대 그렇게 생각하지 마라. 왜냐하면:

그러니까, 다음장으로 가기전에 이 장을 꼭 읽어달라. 이건 시간 낭비가 아니다.

[편집] 개요

모델 - 뷰 - 컨트롤러 아키텍쳐(MVC)는 시각 기반 에디터에 적용되는 소프트웨어 아키텍쳐이다. 나중에 이 아키텍쳐의 장점 몇가지를 설명하겠다. GEF의 모든 조각들은 이 아키텍쳐에 따라 그 장소가 결정되어 개발된다. GEF를 사용하여, 이 아키텍쳐에 맞추어 올바르게 개발하는 것은 순전히 당신 몫이다.

역자주 API계약을 모두 지키며 개발 했다 하더라도, 이 아키텍쳐에 맞추어 위치 및 역할을 결정하지 않으면 잘못된 애플리케이션이 개발 될 수 있다는 의미이다. 단순 API보다 아키텍쳐를 완전히 이해하는 것이 중요하다는 것을 강조한다.

[편집] 모델

누구나 이 단어가 의미하는 것에 대해 직관적인 견해가 있다고 생각한다, 그러니 이 단어를 더 명확히 정의하는 시도는 하지 않겠다. 하지만 모델에 대해서 알아야 할 몇가지 필수 사항들이 있다:

Gef desc im5.gif


GEF는 당신이 사용할 모델에 대해 특별한 제약을 두지 않는다. 이는 어떤 종류의 모델이건 대부분 GEF에서 사용할 수 있다는 장점을 의미한다. 반면, 선행 조건을 당신 스스로 관리해야 한다는 뜻이기도 하다! 그리고 어쩌면 통지 시스템 및 이벤트 수신자 등록 매커니즘을 직접 구현해야 할 수도 있다. 하지만 이미 이클립스 및 java.beans 패키지등에서 바로 사용가능한 자원들이 있으므로 쉽게 할 수 있다. 이에 대해 나중에 더 설명하겠다.

[편집]

뷰는 시각적 표현을 구성하는 블록들의 집합이다. MVC 아키텍쳐에 있어, 뷰는 모델에 대해 몇몇 요구조건이 있다:

Gef desc im6b.GIF


이 후의 설명에서, 뷰가 Draw2DFigure 들로 구성되었다고 가정하고 진행하겠다. GEFDraw2D FigureSWTTreeItem에 대한 지원을 기본적으로 갖고 있다, 그러나 나는 두번째 것을 써본적이 없기 때문에 그에 대해 별로 말할 것이 없다. 나는 이 외에도 GEF가 다른 종류의 시각 객체와 함께 쓰일 수 있다고 믿지만, 기본 제공되지 않는다.

역자주 된다, 해봤다.

Draw2D에 대해 간략한 설명을 다음절에 계속하겠지만, 정말 필요한 것은 GEF를 이해하는 것이다.

[편집] 컨트롤러

모델이 뷰에 대한 레퍼런스를 갖지 않는 것 처럼, 뷰도 모델에 대한 레퍼런스를 갖지 않는다, 아마도 이 둘을 무엇이 연결하는지 궁금할 것이다. 이 연결은 바로 컨트롤러에 의해 만들어 진다. GEF에서 이 컨틀롤러들은 EditPart의 자식 클래스이다.

각각의 모델과 이 모델이 표현되는 뷰 사이마다 EditPart가 존재한다. EditPart는 양쪽 모두에 대해서 알고 있으며(모델과 뷰에 대한 레퍼런스를 가진다), 따라서 모델로 부터 정보를 얻어 뷰의 속성들을 업데이트 할 수 있다. EditPart는 모델을 리스닝하면서, 변경을 통지 받으면 모델 객체의 새로운 상태를 어떻게 뷰에 반영해야하는지 알고 있어야 한다. 컨트롤러는 모델 편집과정중에 여러가지 방법으로 직접 참여하기도 한다. 이는 나중에 더 설명된다.

[편집] 모델, 뷰, 컨트롤러의 협력

지금까지 설명한 것을 종합해 그림으로 나타내면 다음과 같다:

Gef desc im7.gif

자 이제, 모델의 변경 결과를 어떻게 뷰에 반영할 것인가에 대한 아이디는 확보되었고, 누가 그일을 해야하는지도 알고 있다. 그러나 사용자의 액션을 모델 변경으로 번역하고 모델에 적용하는 과정은 여전히 불투명하다. 이 과정에는 앞으로 더 설명해야할 수많은 것들이 관여한다.

이 그림을 보고 생각해야 할 문제는 GEF가 어떻게 굴러먹는지에 관한것이 아니라, 어떻게 GEF의 다른 퍼즐 조각들을 짜 맞추어야 할 것인가 하는 문제이다.

[편집] 왜 당신이 이 아키텍쳐를 써야 하는가

우선, GEF는 그렇게(MVC를 써서) 사용하도록 의도되어 만들어 졌다. 그러니 그렇게 안하면, 수 많은 문제를 겪게 될 것이다.

역자주 그렇게 하지 않아 발생하는 문제들에 대해 제발 질문하지 말아달라.

이 아키텍쳐를 사용함으로써 얻는 이득은 매우 많다, 지금 몇 가지 당장 떠오르는 것들을 나열하면:

[편집] Draw2D에 대한 간략한 설명

Draw2D에대해 설명을 하긴 하겠지만, GEF를 이해하는데 필요한 기본적인 지식만 언급할 것이다. 좋은 GEF 에디터를 만들 기 위해서는 좋은 GUI가 필수적이다, 하지만 아래의 설명정도로 충분하다고 본다.

[편집] 무엇인가?

Draw2DSwing과 마찬가지 철학[1]을 따르는 경량 UI 컴포넌트 시스템이다. 하지만 Swing이 독자적인 MVC 아키텍쳐를 가지고 있는 것과 달리, Draw2D는 대부분 순수 시각적인 요소들로 구성되어 있다. Draw2D에는 화면을 통해 눈에 보이지 않는 모델은 존재하지 않는데. 그런 이유로 JTable, JList, JTextField나 그런 종류의 모델을 필요로하는 컴포넌트는 존재하지 않는다. 순전히 화면에 무엇인가를 표시하는 목적만 가지고 있으며, 데이터를 보존하거나 다루지 않는다.

[편집] Figure

[편집] Figure의 계층 구조

FigureDraw2D의 최소 구성 재료이다. Draw2D GUI는 이런 Figure를 트리 형태로 구축함으로써 정의되고, 경량 시스템이 이를 GUI에 그림으로서 표현한다.


Gef desc im8.gif

여러분은 parent.add(child)를 호출함으로써 자식을 추가하거나 parent.remove(child)를 호출하여 자식을 제거할 수 있다.

하지만, 이 트리구조가 의미하는 것은 무엇인가? Figure A또는 Figure B의 자식으로 특정 Figure를 추가하는 것은 어떤 변화를 가져오는가? 이는 생각보다 많은 변화를 일으킨다. 여러분은 나머지 설명과 그림을 통해 이 부분을 이해하게 될 것이다.

Figure 클래스는 어느곳에서든 필요한 기본적인 속성과 행동이 정의된 IFigure 인터페이스를 구현한다. Draw2D는 곧바로 사용 가능한 다양하고 유용한 Figure들을 공급한다.

[편집] Figure 그리기

Figure는 스스로 자신을 그릴수 있는 능력이 있다. (Figure.paint(Graphics)) 메소드는 다음의 순서로 그려진다:

  1. paintFigure(Graphics)를 호출하여 자기 자신을 그린다.
  2. 자식들을 재귀적으로 반복하며 paint(Graphics)를 호출하여 모든 자식들을 순서대로 그린다.
  3. 보더를 그린다.

Figure의 트리구조는 루트 Figure에 의해 재귀적으로 paint(Graphics)가 호출되면서 전체가 랜더링 된다.

마지막 두 단계가 Figure들이 그려지는 순서를 정하고, 따라서 어떤 Figure가 위에 그려지는지가 결정된다:

Gef desc im9.gif

Figure 트리 구조에서, A가 B의 자식이라면, A가 B보다 위에 그려진다는 뜻이된다.

[편집] Figure 바운즈

Figure들 마다 바운즈(Bounds)가 사용되는 방식이 다르기 때문에, 바운즈에 대해 쉽게 설명하기는 어렵다.

각각의 Figure는 바운즈를 갖는다. 거의 대부분의 Figure들은 바운즈를 지정할 수 있다. (어떤 Figure들은 내부 컨텐츠에 의해 그 바운즈가 계산되기도 한다) 바운즈는 Draw2D의 클리핑 시스템이, 그 범위 바깥에 무엇인가 그리는 것을 막아주는 사각 영역이다. 이것을 클리핑 영역이라 한다. 각 Figure는 부여된 바운즈내에서 컨텐츠를 최대한 보기좋게 표시해야한다. Figure들에게 그릴수 있는 공간의 제약을 주는 것은 그리기 알고리즘의 성능을 매우 높혀준다.

각각의 Figure와 그와 연결된 클리핑 윈도우에 의해 다음과 같이 그려진다:

Gef desc im10.gif

따라서 A가 B의 자식이라면, A는 B의 바운즈를 벗어난 영역에 그려질 수 없다. B가 그려지는 영역은 A가 그려지는 영역을 완전히 포함한다.

또한 부모 Figure의 바운즈를 지정하면, 모든 자식 Figure도 함께 이동한다. 다시 말해 A가 B의 자식이라면, B가 이동될 때 A도 함께 이동된다.

[편집] 흥미로운 특징들

따라사 A가 B의 자식이라고 한다면, A의 위치는 B의 레이아웃 매니저에 의해 결정됐다고 볼 수 있다.

그러나 이것에 딱히 혼란할 필요는 없다 : GEF는 사용자 액션을 번역할 때 Figure의 리스닝 시스템을 사용하지 않는다.

[편집] 에디터가 열릴 때 뷰 구축하기

GEF가 여러분을 도와주는 첫번째 문제는 시각 에디터(Graphical Editor)를 만드는 일이다. 여러분은 모델을 갖고 있고, 에디터가 열릴 때 맨 처음 이 모델들을 표시할 방법이 필요하다고 가정한다. 사실, 뷰를 처음 만드는 것은 뷰를 업데이트 하는 것과 다른 점이 전혀 없지만, 이해를 돕기위해 분리하여 설명한다.

뷰의 생성에 참여하는 요소들은 다음과 같다:

다시 한 번 강조하는데, 이는 GEF의 블랙박스 내부에서 일어나는 일들에 대한 설명이 아니다: 뷰가 어떻게 만들어지는지 이해해야만 여러분의 필요에 맞게 뷰를 구성하는 코드를 쓸 수 있다. 이 섹션을 읽고 나면 에디터가 열릴 때 여러분의 모델을 위한 뷰를 자동으로 구축하는 코드를 쓸 수 있게 될 것이다. 하지만 모델의 변경에 대응해 뷰를 업데이트하는 작업은 이 섹션에 포함하지 않는다. 또한 사용자에 의한 편집 기능도 제공되지 않을 것이다.

[편집] 해야 할 일

뷰에 표시하고 싶은 각 모델에 대해 다음 3개의 작업을 수행해야 한다:

[편집] 모델 객체의 뷰

모델 객체의 뷰가 꼭 하나의 Figure로 이뤄져 있을 필요는 없다. 한 모델에 대해 여러개의 Figure를 조합하여 하나의 뷰를 공급할 수 있다. 따라서 모델에 대한 한 뷰는 전체 뷰의 Figure 트리구조의 부분 트리(Sub Tree)로 볼 수 있다:

Gef desc im32.gif

이 그림을 제대로 보고 이해했다면 의문이 하나 생겼을 것이다 : 컨텐트 팬. 아직까지 이것에 대해 언급한 적이 없다. 미안하지만 이는 나중에 설명 될 것이다. 지금으로선 특정 모델의 뷰에서, 다른 GEF 컨텐츠들을 표시 해야 할 때, 그 컨텐츠를 표시하기 위한 컨테이너 정도로 생각하면 된다. 컨텐트 팬을 딱히 지정하지 않으면 루트가 컨텐트 팬으로 사용된다.

예: 아래는 Person을 표현하는 뷰로, 이름과 호칭을 표시할 수 있다. 인터페이스는 Person View의 속성들을 나열한다:

public interface IPersonFigure extends IFigure{
   public void setName(String name);
   public void setSurname(String surname);
   public IFigure getContentPane();
}


Person을 표현하는 PersonFigure:

public class PersonFigure extends Figure implements IPersonFigure{
   private Label labName;
   private Label labSurname;
   private IFigure contentPane;
   public PersonFigure(){
      setLayoutManager(new ToolbarLayout());
      labName = new Label();
      add(labName);
      labSurname = new Label();
      add(labSurname);
      contentPane = new Figure();
      contentPane.setLayoutManager(new ToolbarLayout());
      contentPane.setBorder(new TitleBorder("fruits list :"));
      add(contentPane);
   }
   public void setName(String name){
      labName.setText(name);
   }
   public void setSurname(String surname){
      labSurname.setText(surname);
   }
   public IFigure getContentPane(){
      return contentPane;
   }
}
역자주 lab는 Label임을 나타내는 접두어
Gef desc im31.gif

이제 여러분은 모델과 뷰를 연관짓은 코드를 어디다 써야 할 지, 어디에서 모델의 상태를 바탕으로, 뷰를 업데이트하는 코드를 써야할지 궁금할 것이다. 우리는 이미 컨트롤러가 모델과 뷰를 연관 짓는다는 사실을 알고 있다. 따라서 여러분은 아마 답을 짐작할 수 있을 것이다 : 이 코드들은 모델과 뷰 사이에 있는 EditPart에 포함되며, 어떻게 하는 지는 다음 섹션에서 설명한다.

[편집] EditPart로 모델과 뷰 연결짓기

아래 그림은 EditPart들이 어떻게 모델과 뷰 사이에 위치하는지 보여준다:

Gef desc im30.gif

EditPart는 모델 객체를 표현하고 있는 Figure 서브트리에 대해 두가지를 알고 있다:

EditPart의 뷰로 Draw2DFigure를 이용하게끔 기본 구현된 추상 클래스는 AbstractGraphicalEditPart이다. 각각 AbstractGraphicalEditPart들은 모델 객체와 뷰를 연결한다. 여러분은 setModel(Object)getModel()를 호출하여 모델을 지정하거나 얻을 수 있다. 또 getFigure()를 호출함으로써 루트 피겨를 얻을 수 있다. 그러나 루트 피겨를 지정할 수는 없으며, 이는 GEF가 필요로 할 때 만들어 진다.

자 이제, 여러분은 표현해야할 다른 종류의 모델들을 가지고 있고, 각각의 모델 타입들을 위한 Figure 클래스들을 작성했다. (인터페이스도 만들었을 수 있다). 또 각각의 모델 유형에 따라 모델과 뷰를 연결해 주는 AbstractGraphicalEditPart의 자식 클래스들도 만들었을 것이다. 일단 GEF가 뷰를 구축하기 위해 오버라이드 또는 구현해야할 메서드들은 다음과 같다. 다른 메서드들은 나중에 언급한다:

createFigure(), getContentPane() 그리고 getModelChildren() 메서드가 protected 접근자를 가진 것을 볼 수 있다. 그러니 그들을 직접 호출할 필요가 없다는 것을 눈치채야 한다, 그들은 GEF에 의해 필요할 때 뷰를 구성하며 호출될 것이다. GEF가 이 메서드들을 어떻게 이용하는 지는 나중에 더 명확히 설명한다, 현재로선 어떤 코드들을 그 안에 집어 넣어야 하는지 이해하는데 집중하라.

[편집] createFigure() 메서드

이전에 다룬 예제를 계속해 나가기 위해, 여러분이 Person 모델 객체를 표현하길 원하며, PersonFigure 클래스 및 IPersonFigure 인터페이스를 앞서 예제와 동일하게 작성했다고 가정한다. 이제 여러분은 AbstractGraphicalEditPart를 상속하여 PersonEditPart를 만들 수 있고, 그를 이용해 Person 모델과 PersonFigure들을 연결할 수 있다. createFigure() 메서드에서 다음과 같이 코드를 작성한다:

public class PersonEditPart extends AbstractGraphicalEditPart{
   ...
   protected Figure createFigure(){
      return new PersonFigure();
   }
   ...
}

[편집] refreshVisuals() method

이역시 단순하다. 여러분은 모델에 대한 레퍼런스를 갖고 있으며, 뷰에 대한 레퍼런스도 가지고 있다. 따라서 모델의 상태를 뷰에 적용하기만 하면 된다:

public class PersonEditPart extends AbstractGraphicalEditPart{
  ...
  public void refreshVisuals(){
    IPersonFigure figure = (IPersonFigure)getFigure();
    Person model = (Person)getModel();
    figure.setName(model.getName());
    figure.setSurname(model.getSurname());
  }
  ...
}

[편집] getContentPane() 메서드

만약 컨텐트 팬을 정의해 두었다면, 여러분의 인터페이스를 통해 컨텐트 팬을 얻을 수 있을 것이다:

public class PersonEditPart extends AbstractGraphicalEditPart{
   ...
   protected IFigure getContentPane(){
      return ((IPersonFigure)getFigure()).getContentPane();
   }
   ...
}

[편집] getModelChildren() 메서드

GEF가 어떻게 뷰들을 구성하는지 완전히 알게 된 후에 이 메서드가 무슨일을 하면 좋을지 명확히 할게 될 것이다. 여기서는 개념을 잡기위해 간단한 일반론만 설명한다.

대다수의 모델은 한 뷰가 다른 자식 뷰들을 가짐으로써, 포함 계층구조로 표현된다. (이 말이 꼭 모델역시 트리구조여야 한다는 의미가 아니다. 여기는 뷰에 관한 이야기만 하겠다) UML 다이어그램을 떠올려 보자. 다이어그램은 클래스들을 포함하며, 각 클래스들은 속성이나 메서드들을 포함하고, 메서드들은 파라미터를 포함하고 있다. 각각의 EditPart들은 getModelChildren()을 오버라이드 해 모델 목록을 리턴함으로써, 이 EditPart의 컨텐트 팬에 표현될 자식 모델들을 GEF에 알려준다. GEF는 각 자식 모델들에 대한 EditPart들을 만들어 자식으로 추가 하며, 각 자식들의 뷰는 컨텐트 팬에 부착된다.

이 예제에서는 과일(Fruit)객체들을 PersonEditPart의 컨텐트 팬에 표시할 것이다, 따라서 우리는 getModelChildren()을 다음과 같이 구현한다:

public class PersonEditPart extends AbstractGraphicalEditPart{
   ...
   public List getModelChildren(){
      Person model = (Person)getModel();
      // null을 리턴 해서는 안된다. 자식 모델이 없는 경우에는 
      // List.EMPTY_LIST 등과 같은 내용이 비어있는 리스트를 리턴해야 한다.
      return model.getFuits();
   }
   ...
}

[편집] EditPartViewer, RootEditPart

EditPartViewer 는 SWT 컨트롤 위에 여러분의 모델에 대한 뷰를 설치할 책임을 진다. 여러개의 EditPartViewer를 만드는데 제약은 없다, 각 뷰어는 고유의 EditPart들과 Draw2D Figure들을 갖는다. 그리고 이 뷰어들은 SWT Control 위에 설치 될 수 있다. 여러분의 뷰가 Draw2D Figure로 구성된 경우라면, GraphicalViewer를 상속받아야 한다. 그리고 이 뷰어에 Control, EditPartFactory, RootEditPart 들을 설정해 주어야 하며, 컨텐트라고 불리는 사용자 화면에 표시되게 될 모델 역시 설정해 주어야 한다.

편집기능과 관련해서 책임져야할 요소들이 몇몇 더 있지만, GEF의 다른 요소들을 소개한 후에야 그 역할들을 설명하는 것이 가능해지므로 추후에 언급한다.

EditPart들은 트리 구조로 조직된다. 모델의 뷰들이 구성되는 순서를 이해하면, 이 구조에 대해 이해할 수 있다. 이 구조의 루트가 RootEditPart이다. RootEditPart의 뷰는 특성에 따라 다른 계층의 피겨를 표시하기 위한 포개어진 Draw2D Layer들로 구성된다. 따라서 어떤 RootEditPart를 선택하느냐에 따라, 스크롤, 스케일등과 같은 몇몇 특징도 선택하게 된다. GEF는 약간의 RootEditPart 구현들을 공급한다. 각각의 Javadoc을 읽어 보고 필요에 맞는 것을 선택할 수 있다.

여기에 대해 너무 걱정하지 마라, 이 부분은 전체 흐름에 있어서 중요한 부분도 아니고, 수 많은 예제들 중에서 그냥 붙여 넣기해도 된다. 지금으로선 모델, EditPart들, FigureEditPartFactory들을 이해하는 것이다.

Gef desc im13.gif

EditPartViewer의 다른 역할들:

예:


Construct-icon.png 이곳에 GraphicalEditorPart의 구현 예제가 포함되어야 함: GraphicalViewer의 설치 및 루트 파트 선택, 모델 보여주기 등등

[편집] EditPartFactory

특정 EditPartViwer에 설치된 뷰들이 구성되거나 갱신되는 동안, GEF는 그 EditPartViewer 안에서 주어진 모델을 표현할 수 있는 적합한 EditPart를 만들어야 할 때가 생긴다. 이때 어떤 EditPart를 만들어야 할지 EditPartFactory에게 물어본다. 따라서 EditPartViewer마다 EditPartFactory를 가져야 하고 이를 구현해야 한다. 다음은 GEF에서 제공하는 EditPartFactory의 인터페이스 및 예제이다.

EditPartFactory 인터페이스:

public interface EditPartFactory{
   EditPart createEditPart(EditPart context, Object model);
}

예제 :

public interface MyEditPartFactory implements EditPartFactory{
   EditPart createEditPart(EditPart context, Object model){
      EditPart part = null;
      if(model instanceof Diagram){
         part = new DiagramEditPart();
      }
      else if(model instanceof Person){
         part = new PersonEditPart();
      }
      else if(model instanceof Fruit){
         part = new FruitEditPart();
      }
 
      part.setModel(model); // GEF 가 모델을 자동으로 지정해주지 않는다!
      return part; // <-- null 을 리턴하면 안된다.
   }
}

[편집] GEF가 뷰를 구축하는 과정

이제 GEF가 뷰를 어떻게 만드는지 알면 된다. 우선, 무엇이 만들어지는지, 여러분의 코드가 어떤식으로 개입하는지를 설명하고, 뷰 구성과정의 세부사항을 설명하겠다.

다음은 개략적인 진행과정을 설명한다:

Gef desc im14.gif

뷰를 구성하는동안 일어나는 일을 순차적으로 설명할 것이다. 이 과정은 한 에디트 파트가 다른 에디트 파트를 자식으로 추가할 때 발생하는 재귀적 프로세스이며 시작은 뷰어의 RootEditPart 부터 출발한다. 이절에서 설명할 내용이 정확히 구현되는 방법에 대한 설명은 아님을 명심하라.

  1. 컨텐츠라고 불리는 특정 모델이 EditPartViewer에 전달 됨.
  2. EditPartFactory가 주어진 모델에 대해 EditPart를 생성한다. 생성된 EditPart는 컨텐트 EditPart라고 부른다.
  3. 컨텐트 EditPart가 Figure를 만들기 위해 createFigure()와 refreshVisuals() 메서드를 차례로 호출한다. 그후 Figure가 RootEditPart의 적당한 레이어에 부착된다.
  4. 어떤 모델들이 자식으로 표현되어야하는지 알기 위해 EditPart의 getModelChildren() 메서드가 호출된다.
  5. getModelChildren()이 리턴한 각 모델들은 EditPartFactory를 거쳐 적절한 EditPart가 만들어진 뒤, 자식 컨텐트 EditPart로 추가된다.
  6. 추가된 각각의 자식 EditPart의 createFigure()와 refreshVisuals()가 차례로 호출되고, 자식의 Figure를 컨텐트 팬(Content Pane)에 부착한다.
  7. 추가된 자식 EditPart들의 getModelChildren()이 다시 호출되어 전체 EditPart들과 Figure들이 만들어질 때까지 계속 된다. (이 과정은 모든 getModelChildren() 호출이 빈 리스트를 리턴할 때 까지 계속 된다.)


EditPartFactory를 쓰지 않는 다른 방법:

각각의 EditPart는 주어진 모델 리스트에 대해 각각의 자식 EditPart들을 만들고 컨텐트 팬에 그들을 표시해야할 책임이 있다. 이러한 작업은 EditPart.createChild(Object model) 메서드를 통해 일어난다. 이 메서드는 기본적으로는 GraphicalViewer의 EditPartFactory를 얻어 이 작업을 위임한다. 이 기본동작이 마음에 들지 않는다면, createChild() 메서드를 오버라이드 하여, 주어진 모델에 대한 자식 EditPart를 직접 만들 수 있다.

[편집] 모델에 대한 뷰 정의

이 섹션은 뷰가 구성되는 과정과 그 결과에 관여하는 방법을 설명한다.

EditPartFactory는 주어진 모델 객체와 연관될 EditPart 클래스를 정의한다, 또 각각의 EditPart들은 createFigure() 메서드를 통하여 어떤 형태의 뷰가 공급될 것인지 정의하고, refreshVisual()을 통해 모델의 상태를 뷰에 반영하는 방법을 정의한다. 따라서 EditPartFactory와 createFigure(), refreshVisuals()를 정의함으로써, 각각의 모델과 뷰를 연결할 수 있으며, 모델의 상태와 뷰의 표시상태(시각적 속성들)를 연결할 수 있다.

getModelChildren() 메서드는 뷰의 구조를 정의 한다. 예를 들어 특정 모델이 다른 모델 객체의 내부에 표현된다. 따라서 getModelChildren() 메서드를 정의함으로써, 뷰의 구조를 정의 할 수 있다.

[편집] 어떤 모델을 EditPart로 빼야하지?

하지만 아직 질문이 좀 남아 있다: 어떤 모델이 EditPart와 그와 연관된 Figure로 표현되어야 할까. 또, 어떤 모델이 독립적인 EditPart 없이, 단순히 뷰의 속성으로 표현되어야 할까?

예를 들어 여러분이 Person객체를 표현한다고 할 때, 이름과 호칭에 대해 다음과 같은 두가지 접근 법이 있다:

Gef desc im15.gif

혹은 커다란 EditPart를 하나만 써서 모든 모델과 뷰를 전부 관리하는 아이디어를 낼 수도 있다. (하지만, 이건 매우 나쁜 아이디어이다.)

뭐가 맞게 하는 걸까? EditPart들의 올바른 단위는 어떻게 결정하는가?

선택은 당신에게 달려있다, 하지만 이런 결정을 내리는데 염두에 둬야할 만한 사항들이 몇 개 있다:

[편집] EditPart가 여러개의 컨텐트 팬을 필요로하면 어떻게?

Person의 뷰에서 그 사람이 좋아하는 과일의 목록과 함께, 채소의 목록도 함께 보여준다고 가정하자. 그리고 당신은 이 두 목록이 분리되서 표현되길 원한다. 하지만 GEFEditPart가 하나의 컨텐트 팬을 갖는 것만을 허용한다. 자, 이제 어떻게 할까?

만약 이 두그룹이 다른 컨텐트 팬에서 표현된다면 이런 문제로 고민할 필요가 없다: 두 개의 EditPart를 공급하여 각각 과일과 채소를 컨텐트 팬에 표현하도록 구성하면 된다. 그리고 이 두 EditPart들은 PersonEditPart의 자식 파트가 될 것이고, 이를 통해 리스트를 둘로 나눌 수 있다:

Gef desc im20.gif

이 두 EditPart들은 반드시 Person 모델을 리스닝 하면서, 과일이나 채소가 추가되면 각자 refreshChildren()을 호출해야 한다. (리스트 자체는 너무 멍청해서 스스로 통지할 능력이 없다고 가정하겠다)

하지만 이렇게 나뉘어진 두 EditPart의 모델은 뭐가 되야할까? 그리고 그 모델을 어떻게 만들어야 할까? 이는 그림에 표현되지 않았다.

만약 두 EditPart의 모델로 부모와 동일하게 Person 모델을 이용하면, 같은 모델에 대해 세 개의 EditPart가 생긴다. 이는 문제를 야기하는데 EditPartViewer과 관리하는 맵(모델->ditPart)은 1:1 관계만 허용하기 때문이다. 따라서 동일한 Person 모델에 대해 마지막으로 만들어진 EditPart만 정상적인 관계를 가지게 된다. 만약 과일이나 채소의 리스트가 직접 접근 가능한 독자적인 객체라면 이들을 모델로 사용할 수 있겠지만, 아니면 어떻게 해야 할까? 아래는 그러한 문제를 해결하는 한 방법이다, 이것이 최적의 방법은 아닐지 몰라도 잘 작동한다.

PersonEditPart :

public class PersonEditPart extends AbstractGraphicalEditPart{
  ...
  private Object dummyFruitsListModel = new Object();
  private Object dummyVegetablesListModel = new Object();
 
  public List getModelChildren(){
    List list = new ArrayList();
    list.add(dummyFruitsListModel);
    list.add(dummyVegetablesListModel);
      return list;
    }
 
    public EditPart createChild(Object model){
      EditPart part;
 
      if(model == dummyFruitsListModel)
        part = new FruitsListEditPart();
      else
        part = new VegetablesListEditPart();
 
      part.setModel(model);
      return part;
    }
  ...
}

만약 여러분의 모델이 EMF Ecore 모델이라면, 다음과 같은 임시 모델을 이용하는 것도 좋은 방법이다.

이 모델이 EditPartViewer가 유지하는 맵의 키로 사용된다는 점을 기억하는가? 맵의 키로 사용될 수 있도록 hashCode() 및 equals()를 구현해야함을 잊지 말자.

이 소스 코드는 다음절(뷰 업데이트)에 설명할 내용들을 일부 포함한다.

FruitsListEditPart :

public class FruitsListEditPart extends AbstractGraphicalEditPart implements propertyChangeListener{
  ...
  public void activate(){
    if(!isActive())  {
      // 자기 자신의 모델이 아닌, 부모 EditPart의 모델 Person을 얻는다.
      Person p = (Person)getParent().getModel();
      p.addPropertyChangeListener(this);
    }
    super.activate(); // 항상 부모 메서드를 호출 하는 것을 잊지 말자. 
  }
 
  public void deactivate(){
    Person p = (Person)getParent().getModel();
    p.removePropertyChangeListener(this);
    super.deactivate();
  }
 
  public List getModelChildren(){
    Person p = (Person)getModel();
    return p.getFruits(); 
  }
 
  // EditPartFactory가 Fruit 객체를 어떻게 다뤄야 하는지 알고 있다고 가정했고, 
  // 따라서 createChild()를 오버라이드 할 필요가 없다.
  public void propertyChange(PropertyChangeEvent ev){
    if(ev.getPropertyName().equals(Person.PROPERTY_FRUITS)
      refreshChildren();
    }
    ...
  }
  ...
}

[편집] 모델 변경시 뷰 업데이트하기

[편집] 통지 매커니즘

여러분이 앞서 설명한 MVC 패턴을 아직 기억하고 있다면, 모델은 반드시 자기 자신의 변경을 리스너들에게 통지해야 한다는 사실을 기억할 것이다. 아래는 그를 구현한 Person 클래스의 예제이다:

Person:

import java.beans.*;
import java.util.*;
 
public class Person{
  private String name;
  private List fruits;
  private PropertyChangeSupport listeners;
  public final static String PROPERTY_NAME = "name";
  public final static String PROPERTY_FRUITS = "fruits";
 
  public Person(){
    name = "";
    fruits = new ArrayList();
    listeners = new PropertyChangeSupport(this);
  }
 
  public void setName(String newName){
    String oldName = name;
    name = newName;
    listeners.firePropertyChange(PROPERTY_NAME, oldName, newName);
  }
 
  public void addFruit(Fruit fruit){
    fruits.add(fruit);
    listeners.firePropertyChange(PROPERTY_FRUITS, null, fruit);
  }
 
  public void removeFruit(Fruit fruit){...}
 
  public void addPropertyChangeListener(PropertyChangeListener listener){
    listeners.addPropertyChangeListener(listener);
  }
 
  public void removePropertyChangeListener(PropertyChangeListener listener){...}
 
  public String getName(){...}
 
  public List getFruits(){...}//the returned list should not be modified because
         //no events would be fired in that case
}

[편집] 모델 리스닝

MVC 패턴에서 컨트롤러는 반드시 모델의 변경을 수신(listening) 해야 한다. GEF에서 EditPart들은 컨틀롤러이므로, 모델이 변경되면, 모델의 새로운 상태에 맞추어 뷰를 업데이트하기 위해, 모델을 리스닝 해야 한다.

EditPartEditPart 트리에 추가 되고, FigureFigure트리에 추가된 후, EditPart#activate() 메서드가 호출된다. EditPart가 트리에서 제거 될 때는 deactivate()가 호출된다. 따라서 이 곳은 모델에 대한 훅과 언훅처리를 하기 좋은 곳이다. 아례는 PersonEditPart에 대한 예제이다:

import java.beans.*;
import org.eclipse.gef.*;
import org.eclipse.gef.editparts.*;
public class PersonEditPart extends AbstractGraphicalEditPart implements PropertyChangeListener{
  ...
  public void activate(){
    if(!isActive())
      ((Person)getModel()).addPropertyChangeListener(this);
    super.activate();
  }
 
  public void deactivate(){
    ((Person)getModel()).removePropertyChangeListener(this);
    super.deactivate();
  }
 
  public void propertyChange(PropertyChangeEvent ev){
    ...
  }
  ...
}

EditPartdeactivate 메서드에서 언훅 하는 것을 잊지 말라. 만약 이를 잊으면, 모델이 변경되었을 때, Figure 트리에서 이미 제거된 피겨를 다시 그리려는 시도가 일어나고, 이는 매우 괴상한 에러를 만들어 내기 때문에 몇날 며칠을 끙끙 앓는 수가 있다.

[편집] 모델 변경 이벤트에 반응하여 뷰 업데이트 하기

모델이 이제 이벤트도 좀 찍찍 뱉을 줄 알고, 제법 그럴싸해졌으니 다음의 일들을 해보자:

GEF는 다음의 두 메서드를 통해 이 일을 할 수 있다:

모델의 변경중, 모델의 트리 구조가 변경되는 경우를 구조적 변경(Structural Change)이라고 부른다.

refreshChildren() 메소드는 현재 자식 EditPart들의 모델들과 getModelChildren()의 결과 모델들을 비교한다. 그 후 새로 추가된 모델들에 대해 새 EditPart들을 만들고, 사라진 모델에 대한 EditPart들을 제거한다. 변경이 없는 모델과 그와 연관된 EditPart들은 그대로 둔다.

모델이 변경 사실을 EditPart가 통지 받았을 때, 이 두메서드를 이용하여 적절히 대응 해야 한다.


다음은 PersonEditPart 클래스의 예제이다:

import java.beans.*;
import org.eclipse.gef.*;
import org.eclipse.gef.editparts.*;
public class PersonEditPart extends AbstractGraphicalEditPart implements PropertyChangeListener{
  ...
  public void propertyChange(PropertyChangeEvent ev){
    if (ev.getPropertyName().equals(Person.PROPERTY_NAME))
      refreshVisuals();
    else if (ev.getPropertyName().equals(Person.PROPERTY_FRUITS))
      refreshChildren();
  }
 
  public void refreshVisuals(){
    IPersonFigure figure = (IPersonFigure)getFigure();
    Person model = (Person)getModel();
    figure.setName(model.getName());
    figure.setSurname(model.getSurname));
    super.refreshVisuals();
  }
  ...
}

이 기사는 특집기사:GEF의 이해 2부에서 계속 됩니다. 될 것입니다.

[편집] 참조

  1. Swing의 철학: 한 번 쓰면 어디서든 동일한 모습과 동작을 가진다. 또한 UI는 경량 객체로 일반 모델과 마찬가지로 자유롭게 탈부착이 가능하다. 자세한 내용은 경량 UI 문서를 참조하라.

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

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