특집기사:Xtext로 JVM 연동 언어를 만드는 5단계

위클립스
이동: 둘러보기, 찾기
Article.png 특집기사 정보
원문 보기
범주 Xtext
저자 이클립스 Xtext 공식 문서
역자 이지율

목차

[편집] 개요

이 튜토리얼을 진행하면서, 우리는 도메인 모델 언어를 구현할 것이며, Xtext 2.1에 소개된 JVM 지원 기능을 도입해 볼 것이다. JVM과의 연동은 DSL 언어를 더 매력적으로 만드는 요소이며, 이 튜토리얼을 청사진으로 삼아 여러분들의 프로젝트에도 이런 요소를 추가하는 것을 자유롭게 시도해 보길 바란다.

우리가 정의할 모델 언어는 자바 표현식 및 자바 타입들과의 링크를 지원하며, 자바 소스코드로 변환될 것이다. 문법은 상당히 친숙한 형태로 정의할 것이며, 예제는 다음과 같다:

import java.util.List
 
package my.model {
 
    entity Person {
        name: String
        firstName: String
        friends: List<Person>
        address : Address
        op getFullName() : String {
            return firstName + " " + name;
        }
 
        op getFriendsSortedByFullName() : List<Person> {
            return friends.sortBy( f | f.fullName);
        }
    }
 
    entity Address {
        street: String
        zip: String
        city: String
    }
}

보시다시피, 이 언어는 자바 지너릭, 자바 표현식, 심지어 클로져같은 고급 언어 기능을 제공한다. 쫄지마라, 여러분은 이러한 기능을 직접 구현할 필요가 없고, 이를 가능하게 하는 다양한 인프라를 사용할 수 있다.

이제 우리는 간단한 5단계를 거쳐, 컴파일러를 포함하여 완벽히 작동하는 언어를 만들어 볼 것이다.

우선 Xtext를 설치한 뒤, 이클립스를 다시 시작하여 새로운 워크스페이스를 하나 열자.

[편집] Xtext 프로젝트 만들기

시작하려면 우선 프로젝트를 만들어야 한다. 새 프로젝트 위저드를 열고 새 Xtext 프로젝트 위저드를 선택한다.

File -> New -> Project... -> Xtext -> Xtext project

의미있는 프로젝트 이름, 언어의 이름, 그리고 이 언어가 사용할 파일 확장자를 지정한다, 예:

Main project name org.example.domainmodel
Language name org.example.domainmodel.Domainmodel
DSL-File extension dmodel

Finish 버튼을 눌러 프로젝트를 생성하자.


Xtext-wizard.png


위저드가 성공적으로 실행되었으면, 워크스페이스에 3개의 프로젝트가 보일 것이다.

프로젝트 설명
org.example.domainmodel 언어 정의 및 런타임 컴포넌트(파서, 렉서, 링커, 벨리데이션 등)을 포함한다.
org.example.domainmodel.tests 테스트 유닛들
org.example.domainmodel.ui 에디터 및 워크벤치와 연관한 기능 요소들


Xtext-project-layout.png

[편집] 문법 정의

위저드는 자동으로 Domainmodel.xtext 문법정의 파일을 열어준다. 보면 알겠지만 간단한 예제가 이미 담겨 있다:

grammar org.example.domainmodel.Domainmodel with
                                      org.eclipse.xtext.common.Terminals
 
generate domainmodel "http://www.example.org/domainmodel/Domainmodel"
 
Model:
    greetings+=Greeting*;
 
Greeting:
    'Hello' name=ID '!';

다 지우고, 다음 내용을 붙여 넣자:

grammar org.example.domainmodel.DomainModel with
                                      org.eclipse.xtext.xbase.Xbase
 
generate domainmodel "http://www.example.org/domainmodel/Domainmodel"
 
DomainModel:
    elements+=AbstractElement*;
 
AbstractElement:
    PackageDeclaration | Entity | Import;
 
PackageDeclaration:
    'package' name=QualifiedName '{'
        elements+=AbstractElement*
    '}';
 
Import:
    'import' importedNamespace=QualifiedNameWithWildCard;
 
QualifiedNameWithWildCard :
    QualifiedName  ('.' '*')?;
 
Entity:
    'entity' name=ValidID 
        ('extends' superType=JvmTypeReference)? '{'
        features+=Feature*
    '}';
 
Feature:
    Property | Operation;
 
Property:
    name=ValidID ':' type=JvmTypeReference;
 
Operation:
    'op' name=ValidID 
        '('(params+=FullJvmFormalParameter 
            (',' params+=FullJvmFormalParameter)*)?')'
        ':' type=JvmTypeReference 
        body=XBlockExpression;

이제 하나씩 살펴보자.

역자주 이 튜토리얼은 JVM과의 연동에 비중을 두기 때문에, 자세한 문법 표현식 정의 방법에 대해서 다루지는 않는다.
1.
grammar org.example.domainmodel.DomainModel with
                            org.eclipse.xtext.xbase.Xbase

org.eclipse.xtext.common.Terminals 대신 org.eclipse.xtext.xbase.Xbase를 상속받았음을 주의하자. Xbase는 정적인 자바 표현식이나, 자바 타입 참조등과 같은 현대적인 언어기능을 쉽게 추가하고 재 사용할 수 있게 한다. 경우에 따라서 자바 어노테이션을 지원하고 싶은 경우도 있을 것이다. 이 경우엔 org.eclipse.xtext.xbase.XbaseWithAnnotations를 상속받자.

2.
DomainModel:
    elements+=AbstractElement*;

첫번째 룰(Rule)은 항상 진입 시작 룰이어야 한다. 위 구문은 DomainModel이 임의의 갯수( * )의 AbstractElement들로 구성되어 있으며, 각각은 elements피쳐에 추가( += )될 것임을 나타낸다.

3.
AbstractElement:
    PackageDeclaration | Entity | Import;

AbstractElement 룰은 PackageDeclaration 룰, Entity 룰 그리고 Import 룰로 위임한다.

4.
PackageDeclaration:
    'package' name=QualifiedName '{'
        elements+=AbstractElement*
    '}';

PackageDeclaration는 네임 스페이스를 선언하는 역할을 하며 내부에 다시 임의의 갯수의 AbstractElement들을 포함한다. Xtext는 생성된 모델의 계층 구조 및 스코핑, 그리고 유효 참조이름등과 같은 기능들의 기본 구현을 제공한다. 참조명 기본 구현은 자바와 유사하다. PackageDeclaration 'foo.bar'에 속한 Entity 'Baz'의 참조명은 foo.bar.Baz가 된다. 다른 형태의 이름을 사용하고 싶으면 IQualifiedNameProvider를 독자적으로 구현할 수 있다.

5.
Import:
    'import' importedNamespace=QualifiedNameWithWildCard;
 
QualifiedNameWithWildCard :
    QualifiedName  ('.' '*')?;

Import 룰은 네임스페이스 지원 기능을 사용한다. 자바에서 사용하는 import와 마찬가지의 전체 기능을 단지 이 두 룰을 정의 함으로써 사용할 수 있다.

역자주 여기서 피쳐 명으로 사용된 importedNamespace는 Xbase의 예약어 이다. Xbase는 이 피쳐명을 import로 취급한다. 직접 import 기능을 구현할 것이 아니라면 import 키워드는 원하는데로 바꿔도 괜찮지만, 할당할 피쳐명을 바꾸면 안된다.
6.
Entity:
    'entity' name=ValidID 
        ('extends' superType=JvmTypeReference)? '{'
        features+=Feature*
    '}';

Entity룰은 'entity' 키워드로 시작하며 뒤 이어 이름이 나온다. 'extends' 구문은 괄호로 싸여 있고 반드시 있을 필요는 없다(수량 연산자: ?). 그리고 superType 피쳐의 타입으로 부모 문법에서 정의된 JvmTypeReference를 사용한다. JvmTypeReference은 짧은 클래스 이름 부터, 패키지명을 포함한 Qualified 명칭, 그리고 지너릭 표현식 및 와일드 카드를 포함하여 참조 가능한 모든 자바 타입명을 정의한다. 마지막으로 두 중괄호 사이에 임의의 갯수의 Feature들이 올 수 있다.

7.
Feature:
    Property | Operation;

Feature 룰은 Property 룰과 Operation 룰로 위임된다.

8.
Property:
    name=ValidID ':' type=JvmTypeReference;

Property는 이름(name)을 갖고 다시 상속받은 룰인 JvmTypeReference을 이용한다.

9.
Operation:
    'op' name=ValidID 
    '('(params+=FullJvmFormalParameter 
        (',' params+=FullJvmFormalParameter)*)?')'
    ':' type=JvmTypeReference 
    body=XBlockExpression;

Operation 역시 예상한대로의 시그내쳐를 갖는다. 부모 문법에서 정의된 룰들이 재사용되는 형태를 눈여겨 보라.

Operation의 실제 구현인 'body'는 Xbase에서 가장 자주 이용되는 XBlockExpression에 의해 정의된다. 이는 중괄호에 싸인 임의의 갯수의 자바 표현식으로 구성된다. 예:

{
  return "Hello World" + "!"
}

[편집] 언어 생성

이제 우리는 정의된 문법을 가졌고, 다양한 언어 지원 컴포넌트들을 유도하는 코드 생성기를 실행해야 한다. 문법 편집기에서 컨텍스트 메뉴에서 명령을 찾을 수 있다.

Run As -> Generate Xtext Artifacts

그러면 Xtext 언어 생성기가 수행된다. 이 과정에 파서, 시리얼라이저 등을 비롯한 인프라 코드들이 생성된다. 콘솔뷰에서 이 과정중의 로그를 볼 수 있다.

역자주 이 과정은 MWE에 의해 수행되며, 이러한 과정 자체를 직접 제어하거나, Xtext가 아닌 다른 종류에 프로젝트에 적용해 보고 싶은 사람은 MWE 문서를 참조할 수 있다.


Xtext-generator.png

[편집] JVM 매핑 정의

어떤 언어가 의미를 가지고 쓸만해 지려면, 문법적인 요소만으로는 부족하다. 이제 우리는 도메인에 특화된 컨셉과 다른 언어 요소를 연결해서 Xtext가 이를 어떻게 실행할 것인지 알려줘야 한다. 이 과정은 보통 도메인 모델로부터 다른 언어의 소스파일을 유도하는 생성기를 정의하거나, 도메인 모델을 해석하는 인터프리터를 정의한다. 그러나 Xbase를 사용하는 언어는 IJvmModelInferrer를 이용하여 이 과정을 생략할 수 있다.

주 아이디어는 당신의 언어 컨셉을 자바 클래스나 인터페이스, 어노테이션, enum, 같은 자바 타입들로 번역하는 것이다. 마지막으로 여러분은 언어 개발자로서, 자바 언어에 맞는 정확한 모델을 만들 책임이 남아있다.

여러분의 언어 개념을 자바 요소로 매핑함으로서, 어떤 종류의 스코프에서 다양한 표현식들의 생명주기, 의도된 리턴타입을 결정할 수 있다. Xtext 2.1은 자바 모델을 가독성이 있는 자바 소스코드로 바꿔주는 코드 생성기를 포함한다.

이미 'Generate Xtext Artifacts'를 실행 했다면, org/example/domainmodel/jvmmodel/DomainModelJvmModelInferrer.xtend에서 스텁을 찾을 수 있을 것이다. 그 파일의 내용을 다음으로 교체한다:

package org.example.domainmodel.jvmmodel
 
import com.google.inject.Inject
import org.eclipse.xtext.common.types.JvmDeclaredType
import org.eclipse.xtext.naming.IQualifiedNameProvider
import org.eclipse.xtext.util.IAcceptor
import org.eclipse.xtext.xbase.jvmmodel.AbstractModelInferrer
import org.eclipse.xtext.xbase.jvmmodel.JvmTypesBuilder
import org.example.domainmodel.domainmodel.Entity
import org.example.domainmodel.domainmodel.Operation
import org.example.domainmodel.domainmodel.Property
 
class DomainModelJvmModelInferrer extends AbstractModelInferrer {
 
  /**
   * a builder API to programmatically create Jvm elements 
   * in readable way.
   */
  @Inject extension JvmTypesBuilder
 
  @Inject extension IQualifiedNameProvider
 
  def dispatch void infer(Entity element, 
                IAcceptor<JvmDeclaredType> acceptor, 
                boolean isPrelinkingPhase) {
 
    acceptor.accept(element.toClass(element.fullyQualifiedName) [
      documentation = element.documentation
      for (feature : element.features) {
        switch feature {
          Property : {
            members += feature.toField(feature.name, feature.type)
            members += feature.toSetter(feature.name, feature.type)
            members += feature.toGetter(feature.name, feature.type)
          }
          Operation : {
            members += feature.toMethod(feature.name, feature.type) [
              for (p : feature.params) {
                parameters += p.toParameter(p.name, p.parameterType)
              }
              documentation = feature.documentation
              body = feature.body
            ]
          }
        }
      }
    ])
  }
}

코드를 통해서 아이디어 어떻게 굴러가는지 살펴보자. (이 코드에서 사용된 API에 대해서는 JavaDoc을 참조하라, 특히 JvmTypesBuilder를 눈여겨 보라.)

역자주 이 소스코드가 Java가 아닌 Xtend로 된 것이 부담스러울 수 있다고 이해한다. 하지만 Xtend는 그 유용성에 비해 배우기 쉬운 도구이며, Xtext에서 주로 사용되는 언어이니 만큼 함께 익혀두자.
1.
def dispatch void infer(Entity element, 
          IAcceptor<JvmDeclaredType> acceptor, 
          boolean isPrelinkingPhase) { // (1)

'dispatch' 키워드는 이 메서드가 Entity 타입에 의해서만 호출될수 있도록 한다. Xtend 문서의 디스패치 함수를 참조하여 자세한 매커니즘을 알 수 있다. AbstractModelInferrer를 상속받는 것은 직접 우리 모델을 일일히 돌아다닐 필요가 없게 한다.

2.
acceptor.accept(element.toClass(element.fullyQualifiedName) [ 
...

모델 추론에 생성하는 모든 JvmDeclaredType들은 인식되기 위해 억셉터로 전달되야 한다. 확장 메서드 toClassJvmTypeBuilder로 부터 공급된다. 이 클래스는 가독성이 매우 뛰어난 코드를 작성할 수 있게 하는 다른 많은 확장 메서드들을 공급한다. 대게의 메서드는 마지막 인자로 초기화 블록을 받으며, 그 안에서 현재 생성중인 모델은 묵시적 변수인 it을 통해 참조된다. 이들을 이용하여 자바 엘리먼트를 생성하고 추가적으로 초기화 할 수 있다.

3.
documentation = element.documentation

새로 생성된 자바 엘리먼트에 JavaDoc을 할당한다. 이 할당은 JvmTypesBuilder#setDocumentation(JvmIdentifiableElement element, String documentation)으로 번역된다. 그리고 element.documentation은 실제로는 JvmTypesBuilder#getDocumentation(EObject element)를 호출하게 된다. 이러한 Xtend의 확장 메서드 매커니즘은 Xtend문서의 확장 메서드절에 잘 설명되어 있다.

4.
for (feature : element.features) {
  switch feature { // (4)
    Property : {
      // ...
    }
    Operation : {
      // ...
    }
  }
}

이종의 타입이 섞인 리스트를 이터레이팅(iterating)할 때, 스위치 문의 타입 가드(type guard)를 쓰면 편리하다. featureProperty 타입인 경우 첫 번째 블록이 수행된다. Operation 타입이라면 두 번째 블록이 실행된다. 블록 내에서 별도의 캐스팅 없이도 타입가드가 지정한 타입으로 참조될 수 있다.

5.
Property : {
  members += feature.toField(feature.name, feature.type) // (5)
  members += feature.toSetter(feature.name, feature.type)
  members += feature.toGetter(feature.name, feature.type)
}

각각의 Property에 대해 우리는 적절한 getter와 setter, 그리고 필드로 만들어 준다.

역자주 membersit.members의 묵시적 표현이다. 여기까지 잘 따라왔었는데, 갑자기 코드를 전혀 읽을 수 없다면, 부디 Xtend 문서를 정독해 주길 바란다.
6.
Operation : {
  members += feature.toMethod(feature.name, feature.type) [
    for (p : feature.params) {
      parameters += p.toParameter(p.name, p.parameterType)
    }
    documentation = feature.documentation
    body = feature.body
  ]
}

Operation 들은 상응하는 자바 메서드로 연결된다. documentation이 번역되고, 파라미터들이 초기화 과정중에 추가된다. body = feature.bodyOperation의 body 표현식을 새로 만들어진 자바 메소드의 body로 등록한다. 프레임웍은 필드, 파라미티, 리턴타입들에 대한 가시성을 추론한다.

[편집] 쨘! 에디터!

자 이제 IDE 통합을 테스트 해 볼 차례다. 이미 실행에 필요한 모든 구성을 담은 실행 구성이 생성된 상태이므로 단지 Launch Runtime Eclipse 실행 구성을 실행하기만 하면 된다.

Launch-xtext-editor.png File -> New -> Project... -> Java Projec를 차례로 선택해 우선 새 자바 프로젝트를 만들자. 그리고 우리가 모델 파일의 확장자로 지정한 확장자(*.domodel)로 새 파일을 하나 만들어 보자. 그러면 생성된 에디터가 열릴 것이다. 에디터를 좀 만져보면서 어떤 기능들이 제공되는지 탐색해 보라. 환경설정에서도 여러분의 언어에 대한 항목들을 발견할 수 있을 것이다.

재밌게 놀아!

Xtext-jvm-tutorial-editor.png

[편집] 역자 이야기

겨우 이정도 분량의 내용으로 DSL언어를 정의하고, Java와 통합된 에디터를 공급한다는 것이 좀 허황된 이야기 처럼 들릴 수도 있다. 그러나 이것은 엄연한 현실이며, 미래의 이클립스 기반 프로그래밍은 모두 이렇게 바뀌어 나갈 것이라고 본다.

열심히 읽다가 JVM과의 매핑 부분에서 막힌 독자들이 제법 있을 것 같아 걱정이 된다. Xtend 문서에서 상세한 정보를 얻을 수 있으며 이는 상상 이상으로 유용한 언어이기 때문에, 반드시 Xtend를 먼저 공부하는 것을 권장한다.

추신:

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

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