- 글쓴이 Giuseppe Naccarato

- 번역자 장동석(dsjang@qnsolv.com - http://café.naver.com/deve.cafe)

아파치 벨로시티를 사용한 템플릿 기반의 자동 코드 생성 - 1장#

05/05/2004

상업적으로나 오픈소스로나 여러가지 형태로 제공되는 코드 생성기는 템플릿과 템플릿 엔진을 사용하고 있다. 이 기사에서 필자는 템플릿 기반의 코드 생성과 템플릿과 변환 방법, 그리고 그런 것들을 사용함으로써 얻게 될 막대한 이득에 대해 설명해보고자 한다. 그리고 벨로시티를 사용한 간단한 자바 코드 생성기를 만들어 볼 것이다. 그 코드 생성기는 클래스를 XML로 표현한 XML 파일을 입력으로 받아들여 그 XML이 명시하고 있는 자바 코드를 생성하는 기능을 가지게 될 것이다. 생성과정은 결과물 코드로 뽑아낼 언어의 문법(신택스)을 내부에 포함하고 있는 각각의 템플릿들에 의해 결정된다. 그리고 또한 다른 코드를 생성해내기 위하여 템플릿을 어떻게 사용하는지도 주목해서 보라.

템플릿과 변환#

템플릿을 기반으로 한 변환을 소프트웨어 개발에 있어 엄청나게 광범위하게 사용된다. 많은 툴들이 문서의 포맷을 변환시키기 위하여 사용하기도 한다. XML 파일을 다른 특정 포맷으로 변환시키기 위하여 XSL 템플릿을 기반으로 하여 XSLT를 사용하는 것이 그 한 예라고 할 수 있다. 그림1은 템플릿 변환 과정에 있어 관계있는 4개의 컴포넌트를 보여주고 있다. 그것들은 다음과 같다:

데이터 모델: 특정한 구조를 이루며 데이터를 포함하고 있다. 이 데이터는 변환되어야 할 것들이며 변환 과정 중에 이용된다.
템플릿: 데이터 모델을 특정한 결과 코드로 포맷팅한다. 물론 그것을 위해서 템플릿은 그 내부에 데이터 모델의 레퍼런스를 가지고 사용하게 된다.
템플릿 엔진: 변환 과정을 직접 수행하는 어플리케이션이다. 입력으로 데이터 모델과 템플릿을 받아들여 템플릿에 적힌 데이터 모델의 레퍼런스를 실제 데이터 모델의 값으로 치환하여 템플릿을 통한 변환 과정을 수행한다.
결과물: 위 변환 과정을 거친 뒤 나오는 결과물
figure1.gif

그림 1.템플릿 기반 변환

다음의 데이터 모델의 예를 한번 보자.:

#person.txt
 
$name=길동
$surname=홍

홍길동씨의 이름과 성을 포함하고 있는 텍스트 파일이다. 우리는 다음과 같은 형태의 템플릿을 통해 이것을 변환시키고 싶어졌다.:

#person.template

안녕하시오. 난 $surname 가고 이름은 $name 이오. 템플릿또한 텍스트 파일이다. 두개의 데이터 모델에 대한 레퍼런스를 포함하고 있는데 $name과 $surname이란 레퍼런스가 그것이다.

만약 템플릿 엔진 어플리케이션 이름이 transform이라면 다음과 같이 데이터 모델과 위의 템플릿을 넘겨주어 다음과 같이 변환을 수행할 수가 있다.:

> transform person.txt person.template
결과물은:

안녕하시오. 난 홍 가고 이름은 길동 이오. 템플릿 엔진은 데이터 모델로부터 얻어낸 “홍길동” 이라는 값을 $name 과 $surname 의 레이블을 치환하였다.

한마디: 템플릿 기반의 변환에서 가장 중요한 점은 어떠한 결과물을 뽑아내는 프로그램 그 자체를 건드리지 않고도 결과물의 포맷을 맘대로 바꿀 수 있다는 점이다. 결과물을 바꾸고 싶으면 단지 해야 할 일은 템플릿을 조금 수정하는 것 뿐이다. 여기 다음의 템플릿을 한번 보라.:

#person2.template
 
***********************
이름 = $name
성 = $surname
***********************

여기서는 위에서 사용했던 똑 같은 데이터 모델과 새로운 템플릿 사용하여 변환 과정을 수행해 보겠다.:

> transform person.txt person2.template

결과로는:

***********************
이름 = 길동
성 = 홍
***********************

위에서 보았던 것과 같이 단지 템플릿만 수정함으로 결과를 조작할 수가 있다.

템플릿 기반 코드 생성#

코드 생성과정또한 분명하게 위에서와 같은 변환 과정이다. 데이터 모델은 뽑아낼 코드의 중심이 되는 엔티티의 정보를 담고 있는 것이라고 볼 수가 있고 템플릿을 뽑아낼 결과물 코드의 언어 문법을 표현한다고 볼 수가 있다.

또다른 작은 예로는 언어 독립적인 코드 생성을 보여준다. 여기 예를 보라.:

#student.txt
 
$name=Student
$base=Person

이 파일은 클래스 이름과 클래스의 베이스 클래스(부모 클래스)를 명시하고 있다. 이것을 기반으로 자바 코드 즉 Student 클래스를 뽑고 싶으면 다음과 같은 템플릿을 사용하면 된다.:

#javaclass.template
 
public class $name extends $base {
}

위의 데이터 모델과 템플릿을 입력받은 템플릿 엔진은 다음과 같은 변환 결과를 출력할 것이다.:

public class Student extends Person {
}

Student 클래스의 정의 코드다. 우리는 데이터 모델과 템플릿으로부터 코드 생성을 시작할 수가 있다.

인터페이스를 만들려면 템플릿을 조금만 바꾸면 된다.:

#javainterface.template
 
public interface $name implements $base {
}

위의 템플릿을 템플릿 엔진에다 넣으면 다음과 같은 결과가 출력된다.

public interface Student implements Person {
}

엔진은 Student를 클래스가 아닌 인터페이스로도 뽑게 해 준다. 여기에서의 장점은 데이터 모델이나 템플릿 엔진 둘다 건드릴 필요 없이 이 작업을 했다는 것이다.

그리고 언어 독립성을 보여주기 위해 C++코드를 만들 수 있게 템플릿을 작성해 보자.:

#cpp.template
 
class $name : public $base 
{
}

결과물은:

class Student : public Person 
{
}

C++ 에서의 Student클래스 정의가 나왔다. 물론 데이터 모델이나 엔진을 고치지 않았다. 우리는 똑 같은 것을 기반으로 템플릿만 바꿔서 다른 언어의 코드를 만들어 낸 것이다! 이건 주목할만한 것인데 잘 디자인되고 템플릿을 기반으로 하는 코드 생성기는 단지 템플릿만 바꿔도 다른 언어의 코드를 생성할 수 있도록 해 준다. 그래서 우리는 언어에 종속적이지 않고 잘 디자인된 객체 모델을 만들어야 하는 것이다.

아파치 벨로시티#

이전 예제에서 $로 시작되는 문자열을 데이터 모델로 치환시켜주는 가상의 간단한 템플릿 엔진을 가정하고 설명을 해왔다. 이제 진짜 코드를 생성하기 위해서 진짜 템플릿 엔진을 사용해 보도록 하자.

아파치 벨로시티 템플릿 엔진은 자카르타 오픈 소스 툴이다. VTL(벨로시티 템플릿 랭귀지) 라고 불리는 간단한 템플릿 언어를 기반으로 동작한다. 벨로시티는 자바를 기반으로 동작하도록 제작되었으며 벨로시티 컨텍스트에 연결된 일반 자바 클래스를 데이터 모델로써 사용한다. 변환 과정은 템플릿과 컨텍스트를 입력으로 받아 템플릿에 명시된 포맷대로의 결과물을 내는 것으로 이루어진다. 변환 과정 중에 템플릿의 레이블들은 컨텍스트에 포함된 실제 데이터로 치환된다.

나는 샘플 코드를 J2SE 1.4.2 와 벨로시티 1.4 를 기반으로 작성했고 테스트도 해 보았다. 이를 사용하기 위해서는 클래스패스에 두 개의 jar 파일 - velocity-1.4-rc1.jar and velocity-dep-1.4-rc1.jar. – 을 포함해야 한다.

실전에서의 벨로시티#

다음 예제는 벨로시티를 사용한 간단한 코드 생성기인데 멤버 변수와 접근자 메소드(get/set 메소드) 를 포함한 간단한 자바 클래스를 생성하는 기능을 갖고 있다. 데이터 모델은 클래스들의 이름과 각 클래스들의 애트리뷰트 이름을 명시하고 있는 XML 파일로써 제공된다 여기 입력 데이터 모델 파일을 한번 보라.:
<?xml version="1.0" encoding="UTF-8"?>
<!-- order.xml --!>
<Content>  
        
  <Class name="Customer">
    <Attribute name="code" type="int"/>
    <Attribute name="description" type="String"/>
  </Class>
    
  <Class name="Order">
    <Attribute name="number" type="int"/>
    <Attribute name="date" type="Date"/>        
    <Attribute name="customer" type="Customer"/>        
  </Class>    
 
</Content>

XML 파일 구조는 뭐 말할 것도 없이 쉽다. <Class> 엘레먼트는 클래스들 의미하며 <Attribute> 요소는 클래스의 데이터 멤버를 의미한다. 예제는 Customer 클래스와 Order 클래스를 명시하고 있다. 코드 생성기는 이 문서를 읽어들여 Customer클래스와 Order클래스를 생성해낼 것이다.

다음으로 생성기를 구현해 보자. 첫번째 할 일은 XML데이터 구조를 표현하기 위한 내부 구조를 만드는 것이다. 그러기 위해 두개의 자바 클래스를 만들 것인데 위의 <Class>와 <Attribute>요소를 표현하기 위한 것이다. <Class> 를 표현하기 위한 디스크립터 클래스는 다음과 같다.:

// ClassDescriptor.java
package com.codegenerator.example1;
 
import java.util.*;
 
public class ClassDescriptor {
  private String name;
  private ArrayList attributes = new ArrayList();
  public void setName(String name) {
    this.name = name;
  }
  public String getName() {
    return this.name;
  }
  public void addAttribute(AttributeDescriptor attribute) {
    attributes.add(attribute);
  }
  public ArrayList getAttributes() {
    return attributes;
  }
}

ClassDescriptor클래스는 <Class> 요소에 명시된 데이터를 담기 위해 사용된다. Name 애트리뷰트는 클래스 이름을 담기 위해 사용하고 attributes는 여러 개의 AttributeDescriptor를 담기 위하여 사용되는데 AttributeDescriptor클래스는 ClassDescriptor와 마찬가지로 <Attribute> 요소의 내용을 담기 위해 사용되는 클래스이다. 다음을 보라:

// AttributeDescriptor.java
 
package com.codegenerator.example1;
 
import java.util.*;
public class AttributeDescriptor {
  private String name;
  private String type;
  public void setName(String name) {
    this.name = name;
  }
  public String getName() {
    return this.name;
  }
  public void setType(String type) {
    this.type = type;
  }
  public String getType() {
    return this.type;
  }
}
XML파일을 디스크립터 클래스들에 담기 위해 SAX파서를 사용하는데 다음과 같이 한다.:
package com.codegenerator.example1;
 
import java.util.*;
import javax.xml.parsers.*;
import org.xml.sax.*;
import org.xml.sax.helpers.*;
 
public class ClassDescriptorImporter extends DefaultHandler {
 
  private ArrayList classes = new ArrayList();
 
  public ArrayList getClasses() {
    return classes;
  }
 
  public void startElement(String uri, String name, 
    String qName, Attributes attrthrows SAXException {
 
    // Imports <Class>  
    if (name.equals("Class")) {
      ClassDescriptor cl = new ClassDescriptor();
      cl.setName(attr.getValue("name"));
      classes.add(cl);
    }
 
    // Imports <Attribute>
    else if (name.equals("Attribute")) {
      AttributeDescriptor at = new AttributeDescriptor();
      at.setName(attr.getValue("name"));
      at.setType(attr.getValue("type"));
      ClassDescriptor parent = 
        (ClassDescriptorclasses.get(classes.size()-1);
      parent.addAttribute(at);
 
    }
 
    else if (name.equals("Content")) {
    }
 
    else throw new SAXException("Element " + name + " not valid");
  }  
}

ClassDescriptorImport 클래스가 하는 일은 SAX 기본 핸들러를 익스텐즈하여 XML로부터 디스크립터 클래스로 데이터를 변환하는 것이다. 위에서 볼 수 있듯이, <Class> 요소가 한번씩 처리될 때마다 ClassDescriptor 클래스의 새로운 인스턴스를 만들어내고 핸들러의 ArrayList에 삽입한다. 또한 파서는 <Attribute> 요소를 처리할 때마다 새로운 ClassAttribute클래스를 생성해내고 부모 ClassDescriptor 인스턴스에 그것을 삽입한다. 파싱 작업의 마지막에 classes변수는 XML문서에 명시된 모든 클래스들의 디스크립터 클래스를 가지게 된다.getClasses 메소드는 ArrayList를 반환한다.

이 시점에서 벨로시티가 등장한다. 자바 클래스와 get/set메소드를 만들어내기 위해 작성된 VTL템플릿은 다음과 같다.:

## class.vm
 
import java.util.*;
 
public class $class.Name {
 
#foreach($att in $class.Attributes)   
  // $att.Name
  private $att.Type $att.Name;
  public $att.Type get$utility.firstToUpperCase($att.Name)() {
    return this.$att.Name;
  }     
  public void set$utility.firstToUpperCase($att.Name)($att.Type $att.Name) {
    this.$att.Name = $att.Name;
  }
        
#end
}

$class 라는 레이블은 ClassDescriptor 인스턴스를 가리킨다. 따라서 $class.Name은 결국 ClassDescriptor.getName 를 호출함으로 얻어내는 결과를 출력하는 것이다. ( 실제로 $class.Name 은 $class.getName()의 축약형인데 모든 get 으로 시작하는 메소드에 대하여 이런 축약형이 적용된다.). 해당 ClassDescriptor 에 속한 AttributeDescriptor들의 리스트를 참조하기 위해서는 $class.Attributes 라는 레이블을 사용하면 된다. #foreach 구문은 List인 $class.Attributes에 포함된 모든 항목에 대해 루프를 실행한다. 루프 안쪽의 구문은 $att.Name과 $att.Type에 따른 데이터 멤버와 get/set 메소를 정의하고 있다.

$utility.firstToUpperCase 레이블은 사용자가 정의한 유틸리티 클래스를 호출하게 하는데 인자로 받은 문자열의 첫 글자를 대문자로 바꿔주는 기능을 가지고 있따. 이 메소드는 매우 유용한데 예를 들면 number라는 데이터 멤버로부터 getNumber라는 메소드명을 얻어 낼 때 사용하면 된다.

코드 생성기#

이제 남은건 메인 어플리케이션이다. 그것은 XML파일을 읽어서 디스크립터 클래스에 값을 집어넣고 벨로시티를 호출해 템플릿을 통한 변환 작업을 수행하게 된다.

이 기사에서 ClassGenerator라는 전체 어플리케이션의 소스를 얻어낼 수가 있다. 가장 중요한 메소드는 start() 메소드다. 여기에 구현체가 있으니 한번 보라.:

public static void start(String modelFile, String templateFile)
  throws Exception {
 
  // Imports XML
  FileInputStream input = new FileInputStream(modelFile);
  xmlReader.parse(new InputSource(input));
  input.close();
  classes = cdImporter.getClasses()// ClassDescriptor Array
 
  // Generates Java classes source code
  // by using Apache Velocity
  GeneratorUtility utility = new GeneratorUtility();
  for (int i = 0; i < classes.size(); i++) {
 
    VelocityContext context = new VelocityContext();
    ClassDescriptor cl = (ClassDescriptorclasses.get(i);
    context.put("class", cl);
    context.put("utility", utility);
 
    Template template = Velocity.getTemplate(templateFile);
 
    BufferedWriter writer =
      new BufferedWriter(new FileWriter(cl.getName()+".java"));
 
    template.merge(context, writer);
    writer.flush();
    writer.close();
 
    System.out.println("Class " + cl.getName() " generated!");
  
}

이 메소드는 입력으로 XML와 템플릿 파일명을 받는다. 메소드 내에서는 이전에 설명된 ClassDescriptorImporter 클래스의 cdImporter 인스턴스와 관계지어진 xmlReader 를 사용하여 데이터를 읽어들이게 된다. 당신이 볼 수 있듯이 getClasses메소드를 통하여 클래스로 뽑아내어질 클래스들의 디스크립터 클래스들을 얻어낼 수가 있다. 루핑 코드 안에서 생성되어지는 context객체는 특히 중요한데 왜냐하면 그것은 디스크립터 클래스 인스턴스와 템플릿간의 연결을 제공하기 때문이다. 사실 Context.put 메소드는 자바 객체를 템플릿 레이블과 매핑을 시키게 된다. 다음과 같은 구문을 실행하는 것이 그런 매핑을 수행한다.:
context.put("class", cl);
context.put("utility", utility);

클래스 디스크립터의 인스턴스인 cl 객체는 템플릿에서 $class 레이블로 접근할 수가 있고 유틸리티 클래스는 $utility 레이블을 이용해서 접근할 수가 있다. 마지막의 유틸리티 클래스는 GeneratorUtility 클래스의 인스턴스로 filrstInUpperCase() 메소드를 사용하기 위하여 컨텍스트에 삽입한다.

컨텍스트에 템플릿에서 처리할 데이터를 put한 다음에는 start() 메소드에서 제공받은 대로 특정 템플릿 파일에 대한 Template 객체를 생성하고 merge 메소드를 호출한다. 그러면 템플릿 기반의 변환 작업이 수행된다. 컨텍스트의 데이터는 템플릿에 의해 처리되고 결과는 결과 witer 인스턴스의 스트림에 쓰여지게 된다.

아래 예제에서 코드생성기를 데이터 모델로 ordedr.xml , 템플릿으로 class.vm을 입력으로 사용하여 돌려보면 Customer.java 파일을 얻게 될 것이다. 그 실제 구현 코드를 한번 보자:

import java.util.*;
 
public class Customer {
 
  // code
  private int code;
  public int getCode() {
    return this.code;
  }     
  public void setCode(int code) {
    this.code = code;
  }
        
  // description
  private String description;
  public String getDescription() {
    return this.description;
  }     
  public void setDescription(String description) {
    this.description = description;
  }
 
}

import java.util.*;
 
public class Order {
 
  // number
  private int number;
  public int getNumber() {
    return this.number;
  }     
  public void setNumber(int number) {
    this.number = number;
  }
        
  // date
  private Date date;
  public Date getDate() {
    return this.date;
  }     
  public void setDate(Date date) {
    this.date = date;
  }
 
  // customer
  private Customer customer;
  public Customer getCustomer() {
    return this.customer;
  }     
  public void setCustomer(Customer customer) {
    this.customer = customer;
  
}

결론#

템플릿 기반의 코드 생성기를 개발하면 두 가지 경우에 그것을 사용할 수가 있다.:

  1. 다른 클래스의 생성을 위해 데이터 모델을 변경할 때.
  2. 다른 프로그래밍 언어에 맞는 템플릿을 제공하여 다른 언어의 코드를 뽑아내고 싶을 때.
두가지 사용예는 코드 생성기 자체를 건드리지 않고 수행할 수 있다. 따라서 템플릿 기반의 코드 생성기는 특정 언어의 코드만 뽑아내게 설계된 생성기에 비해 훨씬 더 유연하다.

이 기사의 2장에서는 훨씬 더 복잡한 상황에서의 템플릿 코드 생성을 알아 볼 것이다. 특별히 필자는 Internal Model Object(아래 참고자료 7번)을 사용한 템플릿의 사용법에 대해 알아볼 것이며 특정 언어의 코드 생성에 종속적인 벨로시티 컨텍스트를 비종속적인 컨텍스트로 작성하는 방법에 대한 패턴을 알아볼 것이다.

참고 자료
#

  1. Apache Velocity
  1. F. Aliverti-Piuri, Bug Prevention with Code Generation: A J2EE Case Study, ONJava, March 2004.
  1. J. Herrington, Code-Generation Techniques for Java, ONJava, Sept. 2003.
  1. J. Herrington, Code Generation In Action Manning, 2003.
  1. Code Generation Network
  1. G. Naccarato, Writing a Code Generator in Java JavaPro Online, March 2004.
  1. G. Naccarato, Implement an IOM-Based Code Generator JavaPro Online, March 2004.
  1. Article source code

Giuseppe Naccarato is a software designer/developer and technical writer based in London. He is interested in Java, C++ and C programming, distributed systems, and XML/XSLT. You can reach him at naccarato@inwind.it.


Return to ONJava.com.

Copyright © 2004 O'Reilly Media, Inc.

Add new attachment

Only authorized users are allowed to upload new attachments.

List of attachments

Kind Attachment Name Size Version Date Modified Author Change note
gif
figure1.gif 18.5 kB 1 06-Apr-2006 09:45 이동국
« This page (revision-1) was last changed on 06-Apr-2006 09:45 by UnknownAuthor