Codegenerationwithvelocity |
|
Your trail: CewolfHowTo | Codegenerationwithvelocity | SystemInfo | Links |
- 글쓴이 Giuseppe Naccarato
- 번역자 장동석(dsjang@qnsolv.com - http://café.naver.com/deve.cafe)
05/05/2004
상업적으로나 오픈소스로나 여러가지 형태로 제공되는 코드 생성기는 템플릿과 템플릿 엔진을 사용하고 있다. 이 기사에서 필자는 템플릿 기반의 코드 생성과 템플릿과 변환 방법, 그리고 그런 것들을 사용함으로써 얻게 될 막대한 이득에 대해 설명해보고자 한다. 그리고 벨로시티를 사용한 간단한 자바 코드 생성기를 만들어 볼 것이다. 그 코드 생성기는 클래스를 XML로 표현한 XML 파일을 입력으로 받아들여 그 XML이 명시하고 있는 자바 코드를 생성하는 기능을 가지게 될 것이다. 생성과정은 결과물 코드로 뽑아낼 언어의 문법(신택스)을 내부에 포함하고 있는 각각의 템플릿들에 의해 결정된다. 그리고 또한 다른 코드를 생성해내기 위하여 템플릿을 어떻게 사용하는지도 주목해서 보라.
템플릿을 기반으로 한 변환을 소프트웨어 개발에 있어 엄청나게 광범위하게 사용된다. 많은 툴들이 문서의 포맷을 변환시키기 위하여 사용하기도 한다. XML 파일을 다른 특정 포맷으로 변환시키기 위하여 XSL 템플릿을 기반으로 하여 XSLT를 사용하는 것이 그 한 예라고 할 수 있다. 그림1은 템플릿 변환 과정에 있어 관계있는 4개의 컴포넌트를 보여주고 있다. 그것들은 다음과 같다:
데이터 모델: 특정한 구조를 이루며 데이터를 포함하고 있다. 이 데이터는 변환되어야 할 것들이며 변환 과정 중에 이용된다.
템플릿: 데이터 모델을 특정한 결과 코드로 포맷팅한다. 물론 그것을 위해서 템플릿은 그 내부에 데이터 모델의 레퍼런스를 가지고 사용하게 된다.
템플릿 엔진: 변환 과정을 직접 수행하는 어플리케이션이다. 입력으로 데이터 모델과 템플릿을 받아들여 템플릿에 적힌 데이터 모델의 레퍼런스를 실제 데이터 모델의 값으로 치환하여 템플릿을 통한 변환 과정을 수행한다.
결과물: 위 변환 과정을 거친 뒤 나오는 결과물
그림 1.템플릿 기반 변환
다음의 데이터 모델의 예를 한번 보자.:
#person.txt |
#person.template |
만약 템플릿 엔진 어플리케이션 이름이 transform이라면 다음과 같이 데이터 모델과 위의 템플릿을 넘겨주어 다음과 같이 변환을 수행할 수가 있다.:
> transform person.txt person.template |
안녕하시오. 난 홍 가고 이름은 길동 이오. 템플릿 엔진은 데이터 모델로부터 얻어낸 “홍길동” 이라는 값을 $name 과 $surname 의 레이블을 치환하였다.
한마디: 템플릿 기반의 변환에서 가장 중요한 점은 어떠한 결과물을 뽑아내는 프로그램 그 자체를 건드리지 않고도 결과물의 포맷을 맘대로 바꿀 수 있다는 점이다. 결과물을 바꾸고 싶으면 단지 해야 할 일은 템플릿을 조금 수정하는 것 뿐이다. 여기 다음의 템플릿을 한번 보라.:
#person2.template |
> transform person.txt person2.template |
*********************** |
또다른 작은 예로는 언어 독립적인 코드 생성을 보여준다. 여기 예를 보라.:
#student.txt |
#javaclass.template |
public class Student extends Person { |
인터페이스를 만들려면 템플릿을 조금만 바꾸면 된다.:
#javainterface.template |
public interface Student implements Person { |
그리고 언어 독립성을 보여주기 위해 C++코드를 만들 수 있게 템플릿을 작성해 보자.:
#cpp.template |
class Student : public Person |
아파치 벨로시티 템플릿 엔진은 자카르타 오픈 소스 툴이다. VTL(벨로시티 템플릿 랭귀지) 라고 불리는 간단한 템플릿 언어를 기반으로 동작한다. 벨로시티는 자바를 기반으로 동작하도록 제작되었으며 벨로시티 컨텍스트에 연결된 일반 자바 클래스를 데이터 모델로써 사용한다. 변환 과정은 템플릿과 컨텍스트를 입력으로 받아 템플릿에 명시된 포맷대로의 결과물을 내는 것으로 이루어진다. 변환 과정 중에 템플릿의 레이블들은 컨텍스트에 포함된 실제 데이터로 치환된다.
나는 샘플 코드를 J2SE 1.4.2 와 벨로시티 1.4 를 기반으로 작성했고 테스트도 해 보았다. 이를 사용하기 위해서는 클래스패스에 두 개의 jar 파일 - velocity-1.4-rc1.jar and velocity-dep-1.4-rc1.jar. – 을 포함해야 한다.
<?xml version="1.0" encoding="UTF-8"?> |
XML 파일 구조는 뭐 말할 것도 없이 쉽다. 다음으로 생성기를 구현해 보자. 첫번째 할 일은 XML데이터 구조를 표현하기 위한 내부 구조를 만드는 것이다. 그러기 위해 두개의 자바 클래스를 만들 것인데 위의
ClassDescriptorImport 클래스가 하는 일은 SAX 기본 핸들러를 익스텐즈하여 XML로부터 디스크립터 클래스로 데이터를 변환하는 것이다. 위에서 볼 수 있듯이, 이 시점에서 벨로시티가 등장한다. 자바 클래스와 get/set메소드를 만들어내기 위해 작성된 VTL템플릿은 다음과 같다.:
$utility.firstToUpperCase 레이블은 사용자가 정의한 유틸리티 클래스를 호출하게 하는데 인자로 받은 문자열의 첫 글자를 대문자로 바꿔주는 기능을 가지고 있따. 이 메소드는 매우 유용한데 예를 들면 number라는 데이터 멤버로부터 getNumber라는 메소드명을 얻어 낼 때 사용하면 된다.
이 기사에서 ClassGenerator라는 전체 어플리케이션의 소스를 얻어낼 수가 있다. 가장 중요한 메소드는 start() 메소드다. 여기에 구현체가 있으니 한번 보라.:
컨텍스트에 템플릿에서 처리할 데이터를 put한 다음에는 start() 메소드에서 제공받은 대로 특정 템플릿 파일에 대한 Template 객체를 생성하고 merge 메소드를 호출한다. 그러면 템플릿 기반의 변환 작업이 수행된다. 컨텍스트의 데이터는 템플릿에 의해 처리되고 결과는 결과 witer 인스턴스의 스트림에 쓰여지게 된다.
아래 예제에서 코드생성기를 데이터 모델로 ordedr.xml , 템플릿으로 class.vm을 입력으로 사용하여 돌려보면 Customer.java 파일을 얻게 될 것이다. 그 실제 구현 코드를 한번 보자:
이 기사의 2장에서는 훨씬 더 복잡한 상황에서의 템플릿 코드 생성을 알아 볼 것이다. 특별히 필자는 Internal Model Object(아래 참고자료 7번)을 사용한 템플릿의 사용법에 대해 알아볼 것이며 특정 언어의 코드 생성에 종속적인 벨로시티 컨텍스트를 비종속적인 컨텍스트로 작성하는 방법에 대한 패턴을 알아볼 것이다.
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.
// 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클래스는
// 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;
}
}
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 attr) throws 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 =
(ClassDescriptor) classes.get(classes.size()-1);
parent.addAttribute(at);
}
else if (name.equals("Content")) {
}
else throw new SAXException("Element " + name + " not valid");
}
}
## 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 메소를 정의하고 있다.
코드 생성기
이제 남은건 메인 어플리케이션이다. 그것은 XML파일을 읽어서 디스크립터 클래스에 값을 집어넣고 벨로시티를 호출해 템플릿을 통한 변환 작업을 수행하게 된다.
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 = (ClassDescriptor) classes.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() 메소드를 사용하기 위하여 컨텍스트에 삽입한다.
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;
}
}
결론
템플릿 기반의 코드 생성기를 개발하면 두 가지 경우에 그것을 사용할 수가 있다.:
두가지 사용예는 코드 생성기 자체를 건드리지 않고 수행할 수 있다. 따라서 템플릿 기반의 코드 생성기는 특정 언어의 코드만 뽑아내게 설계된 생성기에 비해 훨씬 더 유연하다.
참고 자료
Attachments:
Go to top Edit this page More info... Attach file...
This page last changed on 25-1월-2005 16:46:00 KST by unknown.