특집기사:SWT, Xtend를 만나다

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

이 문서는 SWT UI를 선언적 언어와 유사한 형태로 작성할 수 있도록, Xtend의 클로져[1]를 응용하는 방법을 소개하고, 그러한 기법들을 재사용하고 공급하는 방법에 대해 설명한다.

목차

[편집] 개요

일을 하던 도중 동료로부터, XtendSWT UI코드를 작성하는 것 비효율적이 되기 쉬운 것 같다는 이야기를 들었다. 왜 그런 생각이 들게 되었는지 묻자, Xtend는 익명 클래스를 지원치 않기 때문에, 리스너의 탈부착이 곤란해지는 경우가 많다는 것이었다.

그래서 나는,어떤 표현식을 갖는 언어로 작성하면, 더 쉽게 SWT UI를 기술할 수 있을지 고민하게 되었고, Xtend의 빌더 문법[2]과 클로져[1]를 이용하면, Xtend 내에서도 꽤 괜찮은 모양의 코드로 나타낼 수 있을 것 같다는 생각이 들었다. 그리고 잠시 작업 후, 다음과 같은 실제 잘 작동하는 결과물을 얻었다:

Xtend 소스 코드 결과
resourceLocator.baseClass = typeof(Hello)
 
shell = Shell[
   text = "Xtend 만세!"
   image = "root.gif"
   layout = GridLayout
 
   Label[text = "하하하"]
 
   Group[
      text = "그룹"
      layout = GridLayout
      layoutData = FILL_BOTH
 
      PushButton[
         image = "delete.gif"
         text = "눌르지마"
         layoutData = FILL_HORIZONTAL
         onClick = [
            text = "누르지 말랬자나"
         ]
      ]
 
      TextField[
         layoutData = FILL_HORIZONTAL
         enabled = false
         text = "랄랄랄"
      ]
 
      Tree[
         layoutData = FILL_BOTH
         RootItem[
            text = "hahaha"
            image = "root.gif"
            SubItem[
               text="하하하"
               image = "delete.gif"
               SubItem[text="호호호"]
               SubItem[text="히히히"]
            ]
         ]
      ]
   ] // 그룹
] // 쉘
 
shell.open
shell.runLoop
Swt-meets-xtend.png

나는 개인적으로

좋은 코드란, 누가 보더라도 무엇을 하는 코드인지 짐작이 가능할 만큼, 의도를 읽어내기 좋은 코드

라고 생각한다. 위의 예제 소스를 보면, 오른쪽의 작동 UI를 상상해 내는 것이 그리 어렵지 않은 일임을 알 수 있다.

그러나 우리에게 친숙한 자바로 같은 일을 하는 코드를 살펴보자:

자바 소스 코드 결과
Display display = Display.getDefault();
 
shell = new Shell();
shell.setText("Xtend 만세!");
shell.setLayout(new GridLayout());
 
Label label = new Label(shell, SWT.NORMAL);
label.setText("하하하");
 
Group group = new Group(shell, SWT.NORMAL);
group.setText("그룹");
group.setLayout(new GridLayout());
group.setLayoutData(new GridData(GridData.FILL_BOTH));
 
final Button pushButton = new Button(group, SWT.PUSH);
pushButton.setText("눌르지마");
pushButton.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
pushButton.addListener(SWT.Selection, new Listener() {
   @Override
   public void handleEvent(Event event) {
      pushButton.setText("누르지 말랬자나");
   }
});
 
Text textField = new Text(group, SWT.BORDER);
textField.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
textField.setEnabled(false);
textField.setText("랄랄랄");
 
Tree tree = new Tree(group, SWT.BORDER);
tree.setLayoutData(new GridData(GridData.FILL_BOTH));
 
TreeItem root = new TreeItem(tree, SWT.NORMAL);
root.setText("hahaha");
 
TreeItem childNode = new TreeItem(root, SWT.NORMAL);
childNode.setText("하하하");
.....
Swt-meets-xtend.png

이 코드를 보고 오른쪽에 보이는 작동 UI의 그림을 떠올리는 것은 쉽지 않은 일이다. (이코드는 절반 이상 생략되었다)

Xtend에서 저런 JSON이나 ExtJS UI 선언 코드와 유사한 형태의 기묘한 코드(읽기는 쉽지만 Java 프로그래머 입장에서 낯선)가 기술될 수 있는 이유는 Xtend의 빌드 문법이라는 특징 때문이다.

[편집] 빌더 문법

Xtend의 빌더 문법[2]이란, 개발자들이 주로 작성하는 코드의 한 패턴 중 하나가:

  1. 객체를 생성하고,
  2. 초기화하고
  3. 기존 객체들과의 유기적인 관계를 설립하는 것

라는 것에서 부터 시작하며, 이를 언어적인 차원에서 지원할 방법을 궁리하며 등장한 것이다.

Xtend가 이를 지원하는 기본 원리는 다음과 같다:

  1. 마지막 인자를 객체를 초기화하는 클로져로 전달 받아, 객체 생성 후 클로져로 객체를 초기화 하는 함수를 빌더 함수라고 한다. 이 함수는 한 번의 호출로 객체의 생성과 초기화를 완료할 수 있다.
  2. 특정 함수의 마지막 인자가 클로져[1]인 경우, 마지막 인자는 괄호 바깥에서 '['와 ']'안에 표현될 수 있다.
  3. 만약 함수를 호출 할 때 괄호안에 아무것도 쓸 것이 없는 경우 괄호 자체를 생략할 수 있다.

이 규칙들에 의해 모델을 빌드하는 코드들은 한결 더 간결해 질 수 있다.

[편집] 예제

  1. def TreeItem newTreeItem(
  2.   TreeItem parentItem, 
  3.   (TreeItem)=>void initializer){
  4.  
  5.   var childItem = new TreeItem(parentItem, SWT::NORMAL)
  6.   initializer.apply(childItem)
  7.   return childItem
  8. }

1newTreeItem 함수는 두 개의 인자를 갖는데, 2첫번째 인자 parentItem은 부모 TreeItem이며, 3두번째인자는 초기화 클로져이다. Xtend에서 첫번째 인자는 주어로 취급 될 수 있으므로, 이제 모든 TreeItem들은 newTreeItem이라는 멤버 변수를 갖게 된다. 따라서 다음과 같은 코드를 호출 할 수 있게 된다.

var TreeItem item = ...
item.newTreeItem([초기화 클로져])

3두번째 인자의 타입은 (TreeItem)=>void로, 이는 TreeItem을 인자로하고, 리턴타입이 void인 클로져를 의미하므로, 다음과 같은 코드를 사용할 수 있다:

var TreeItem item = ...
item.newTreeItem([TreeItem newItem | newItem.text = "자식 아이템"])

Xtend에서 클로져의 인자가 한 개인 경우, 묵시적 파라미터 it[3]을 사용할 수 있고, 3이 클로져는 인자가 TreeItem 한 개 이므로, 묵시적 변수가 적용가능하다. 따라서 다음과 같이 나타낼 수 있다:

var TreeItem item = ...
item.newTreeItem([it.text = "자식 아이템"])

자바에서 this 키워드가 생략 가능한 것 처럼, Xtend에서는 it 키워드를 생략할 수 있으므로 다음과 같이 다시 정리할 수 있다:

var TreeItem item = ...
item.newTreeItem([text = "자식 아이템"])

마지막 인자가 클로져인 경우 괄호 바깥에 쓸 수 있고, 괄호안에 내용이 없는 경우 생략이 가능하므로:

var TreeItem item = ...
item.newTreeItem[text = "자식 아이템"]

위와 같이 다시 정리된다.

초기화 클로져 내부에서 itTreeItem이므로, 다시 newTreeItem을 호출 할 수 있다:

var TreeItem item = ...
item.newTreeItem[
  text = "자식 아이템"
  newTreeItem[text = "손자1"]  newTreeItem[text = "손자2"]]

위 처럼 훨씬 더 간결하고 가독성이 높은 모델 구축 코드를 얻을 수 있다.

[편집] 익명 클래스와 클로져

이 글의 개요에서 언급된 문제중 하나는 Xtend에서 익명 클래스를 기술 할 방법이 없다는 것이다.

예를 들어 아래와 같은 자바코드를 Xtend로 작성하는 것은 불가능하다:

Button b = ...;
b.addListener(SWT.Selection, new Listener(){
  handleEvent(Event e){
    System.out.println("안녕!");
  }
});

Xtend에서 익명 클래스와 가장 근접한 개념을 갖는 요소는 클로져이다. Xtend는 기존 자바 인터페이스를 취급할 때, 다음의 요건을 만족하는 경우, 그 인스턴스를 클로져로 취급한다:

따라서 위의 자바코드는 다음과 같이 Xtend 코드로 나타낼 수 있다:

var Button b = ...
b.addListener(SWT::Selection, [Event e | System::out.println(e.widget + " 버튼이 눌렸습니다.")])

이는 Listener인터페이스가 하나의 메소드만 갖기 때문에 가능하다. 앞서 배운 빌더 문법의 특징을 떠올리면서, 다음과 같이 간추릴 수 있다는 것을 기억하자:

b.addListener(SWT::Selection)[
  System::out.println(widget + " 버튼이 눌렸습니다.")
]

[편집] 메서드가 하나가 아닌 익명 클래스

그렇다면 메서드가 한 개가 아닌 인터페이스의 익명 클래스를 만드려면 어떻게 해야 하는가?

현재 Xtend에서는 불가능하다.

차후 Xtend가 그를 지원할 것인지는 의문이지만, 개인적으로 그것이 Xtend 스타일의 프로그래밍 방법은 아니라고 생각한다. 만약 메서드가 여러개인 하나의 익명 클래스가 있다면, Xtend에서 그것을 여러개의 클로져로 취급할 수 있도록, 중간 자바 클래스나 Xtend 확장을 공급해 주는 것이 한 방법이라고 생각된다.

예를 들어 SelectionListener는 다음과 같이 두개의 메서드를 가지고 있다:

public interface SelectionListener extends SWTEventListener {
  public void widgetSelected(SelectionEvent e);
  public void widgetDefaultSelected(SelectionEvent e);
}

이런 경우, Xtend 스타일로 문제 해결책을 기술하는 방법은 다음과 가깝다:

var Button b = ...
b.onSelected = [System::out.println("사용자가 선택함")]
b.onDefaultSelected = [System::out.println("기본 선택됨")]

물론 ButtononSelected와 같은 속성이 없으므로, 다음과 같이 확장해 주어야 할 것이다.

def setOnSelected(Button button, Listener listener){
  button.addListener(SWT::Selection)[
    listener.handleEvent(it)
  ]
}

확장을 기술하려면 Xtend에서는 사용불가능한 Java 도구들이 필요한 경우도 종종 생기는데, 이에 대해서는 다음절에서 다시 해결책을 설명하겠다.

[편집] 재사용

이제 우리는 초기화 클로져를 이용해 모델 구축을 간단하게 나타내기 위한 1빌더 함수를 만들 줄 알게 되었다. 하지만 이를 다른 팀원에게 어떻게 보급할 것이며, 스스로 어떻게 재사용할 것인가? 모델 구축이 필요한 Xtend 클래스의 소스코드마다 빌더 함수를 붙여 넣을 수도 있겠지만, 더 나은 방법을 알아 보자.

Xtend에서 재사용 방법은 크게 두가지가 있다:

Java의 import구문을 통한 코드 재사용에 대해 설명을 할 필요는 없을테니, Xtend 언어 문맥 확장 기법에 대해서만 설명하겠다.

Xtend에서 언어 문맥을 확장하는 방법은 크게 두가지가 있다:

  1. 정적 메서드 임포트를 통한 확장
  2. 필드를 통한 확장

[편집] 정적 메서드 임포트를 통한 확장

이는 자바가 언어 문맥을 확장하는 방식과 거의 유사하다.

import static java.util.Collections.*;

자바에서는 위의 코드 처럼 static 키워드를 임포트 구문에 추가하면, 같은 소스파일 내에서, Collections가 가진 정적(static) 메서드들을 별도의 추가 문맥 없이 사용할 수 있게 된다.

Xtend도 이와 거의 유사하지만, 선별적으로 메서드를 임포트 할 수는 없고[4], 아스테리크 와일드카드(*)를 이용하여 반드시 모든 메서드를 임포트해야 한다는 점이 다르다. 또한, extension이라는 키워드가 추가로 사용된다. extension 키워드를 사용하지 않으면, 자바와 같은 의미로만 사용된다. 또한 Xtend에서는 임포트 구문뒤에 세미콜론(;)이 붙지 않음을 주의하자.

import static extension java.util.Collections.*

[편집] 자바를 통한 확장 공급

Xtend는 자바보다 뛰어난 점들도 많지만, 그 만큼 제약들도 존재한다. Xtend는 실용성에 중심을 두므로, 자주 사용되는 패턴은 아니지만 이따금 필요한 코딩에 대해 준비가 되어있지 않을 수도 있다. 예를 들면, Xtend에는 double 형 숫자를 기술할 방법이 없다. 반대로 Java에서는 Xtend의 모든 기능을 기술 할 수 있기 때문에, Xtend를 확장 할 때는 Java를 사용하는 경우도 종종 생긴다.

위의 예제와 동일한 역할을 하는 빌더 함수를 자바로 작성하면 다음과 같다:

public MyExtensions{
  public static TreeItem(TreeItem parentItem, Procedure1<TreeItem> initializer){
    TreeItem childItem = new TreeItem(parentItem, SWT.NORMAL);
    initializer.apply(childItem);
    return childItem;
  }
}

이제 Xtend 클래스에서는 위의 확장을 아래와 같이 이용할 수 있다:

// MyExtension을 확장으로 임포트 한다.
import static extension foo.bar.MyExtensions.*
 
class ClientExample{
  def buildTree(){
    var TreeItem item = ...
 
    // 확장을 통해 빌더 함수가 공급된다
    item.newTreeItem[
      text = "자식 아이템"
      newTreeItem[text = "손자1"]
      newTreeItem[text = "손자2"]
    ]
  }
}

자바에는 클로져가 존재하지 않으므로, 이를 위한 인터페이스들이 준비되어 있다. 예제 자바 소스코드의 Procedure1는 리턴 타입이 없고 인자가 한 개인 클로져에 대응하는 자바 인터페이스이다. Xtend/표현식 문서의 함수 매핑절에서 클로져와 자바 인터페이스간에 매핑에 대해 자세한 정보를 얻을 수 있다.

[편집] 정적 메서드 임포트를 통한 확장의 문제점

그런데 import static extension 키워드를 통한 확장 기법은 deprecated 된 상태이다. 작동은 하지만 더 나은 툴링을 위함이라고 한다.

이하의 내용은 순전히 필자의 추론임을 알고 읽기 바란다. Deprecated 된 이유에 관심이 없다면 이 절을 건너 뛰어도 되며,

다른 방식의 장점에 대해서는 다음 절에서 다시금 설명 된다.

Xtend 팀이 말하는 툴링이란 단지 우리가 사용하는 Xtend 에디터와 플러그인들의 품질 측면만을 언급하는 것은 아니다. Xtend로 공급된 확장을 이용하는 측면의 툴링을 포함한 툴링을 의미한다. 후자의 관점에서 정적 확장 방식은 다음과 같은 문제들을 가진다:

[편집] 필드를 통한 확장

필드를 통한 확장은 정적인 메서드들을 임포트 하는 대신, 확장 기능을 가진 자바 객체 인스턴스를 필드로 두어, 확장 역할을 수행하게 하는 것이다. 이 경우 확장 공급 클래스는 정적 메서드들로 기능을 공급할 필요가 없다.

public MyExtensions{
  // static 이 아님.
  public TreeItem(TreeItem parentItem, Procedure1<TreeItem> initializer){
    TreeItem childItem = new TreeItem(parentItem, SWT.NORMAL);
    initializer.apply(childItem);
    return childItem;
  }
}

위와 같은 확장이 존재할 때 클라이언트 클래스의 코드는 다음과 같다:

class ClientExample{
  // 일반 필드를 선언하듯 확장을 선언한다.
  extension MyExtension myExtension = new MyExtension()
 
  def buildTree(){
    var TreeItem item = ...
 
    // 확장을 통해 빌더 함수가 공급된다
    item.newTreeItem[
      text = "자식 아이템"
      newTreeItem[text = "손자1"]
      newTreeItem[text = "손자2"]
    ]
  }
}

일반 멤버 변수 선언과 다른 점은 extension 키워드가 사용되었다는 점이다.

확장 멤버 객체는 언어의 문맥을 확장하는데 사용되므로, 변수명을 가질 필요가 없고, 생략될 수 있다:

extension MyExtension = new MyExtension()

그런데 위와 같이 하면, 공급되는 서비스가 동적으로 변경될 수 없다는 문제점이 있다. 여러 확장이 융합되고, 사용자가 커스터마이징하여 제품을 개발하는 툴링을 공급하는 오픈 엔드 제품의 경우, 이는 문제가 될 수 있다. Xtend 클래스는 자바 클래스로 번역되어 컴파일 되므로, Google Guice와 같이 자바에 적용가능한 인젝터[5]를 이용할 수 있다.

이를 적용하려면 먼저 SPI를 추출한다:

public interface IMyExtension{
  public TreeItem(TreeItem parentItem, Procedure1<TreeItem> initializer);
}
 
public class MyExtension implements IMyExtension { ... }

그 후 클라이언트는 다음과 같이 기술한다:

class ClientExample{
  @Inject
  extension IMyExtension;
 
  def buildTree(){
    ...
  }
}

이제 실제 확장은 인젝터 모듈에 의해 공급되므로, 서비스 공급 구조에 유연성이 확보된다.

인젝터 모듈의 구성은 본 문서의 범위에 포함되지 않으므로, @Inject 어노테이션이 낯 선 사람들은 다음과 같은 문서를 참조하길 바란다:

[편집] 정리

[편집] 참조

  1. 1.0 1.1 1.2 클로져: Xtend/표현식 문서의 클로져절에서 자세히 알 수 있다.
  2. 2.0 2.1 빌더 문법: Xtend/표현식 문서의 빌더 함수절에서 자세히 알 수 있다.
  3. 묵시적 파라미터 it: Xtend/표현식 문서에서 더 자세히 알아볼 수 있다.
  4. 선별적인 메서드 임포트가 불가능한 이유: Xtend디스패치 함수와 같은 기능은 여러 메서드가 합쳐져 하나의 추상화된 분배 메서드를 생성한다. 이런 경우 개별적인 메서드 임포트는 혼란을 가져온다.
  5. 인젝터: 인젝터란 클라이언트를 초기화 하고, 클라이언트가 필요로하는 서비스를 공급하는 임무를 가진 모듈이다. 디펜던시 인젝션 문서에서 자세히 알 수 있다.

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

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