본문 바로가기
카테고리 없음

[Freemarker] Data Model 만들기 (ObjectWrapper, TemplateModel)

by 왕 달팽이 2019. 8. 6.
반응형

Freemarker는 자바 객체를 데이터 모델로 사용할 수 있다. 다만 사용자의 자바 객체를 Freemarker 엔진이 해석할 수 있도록 ObjectWrapper 클래스와 Adapter 클래스를 구현해줘야 한다. FreeMarker 엔진은 ObjectWrapper 클래스와 Adapter 클래스를 통해서 자바객체를 트리형태로 해석할 수 있다.

ObjectWrapper

사용자의 자바 객체를 위한 ObjectWrapper 클래스를 구현하기 위해서는 ObjectWrapper 인터페이스를 구현해야 한다. ObjectWrapper 인터페이스는 다음과 같은 메소드를 가지고 있다.

TemplateModel wrap(Object obj) throws TemplateModelException;

wrap() 메소드는 사용자의 자바 객체를 인자로 받아서 TemplateModel 객체를 리턴한다. TemplateModel 인터페이스는 좀 더 상세한 인터페이스로 확장되어 템플릿 언어에서 사용되는 타입에 매핑된다.

사용자가 ObjectWrapper 인터페이스를 구현한 클래스를 설정하지 않으면 DefaultObjectWrapper 클래스가 사용된다. DefaultObjectWrapper 클래스의 wrap() 메소드 정의는 다음과 같다.

public TemplateModel wrap(Object obj) throws TemplateModelException {
    if (obj == null) {
        return super.wrap((Object)null);
    } else if (obj instanceof TemplateModel) {
        return (TemplateModel)obj;
    } else if (obj instanceof String) {
        return new SimpleScalar((String)obj);
    } else if (obj instanceof Number) {
        return new SimpleNumber((Number)obj);
    } else if (obj instanceof Date) {
        if (obj instanceof java.sql.Date) {
            return new SimpleDate((java.sql.Date)obj);
        } else if (obj instanceof Time) {
            return new SimpleDate((Time)obj);
        } else {
            return obj instanceof Timestamp ? new SimpleDate((Timestamp)obj) : new SimpleDate((Date)obj, this.getDefaultDateType());
        }
    } else {
        Class<?> objClass = obj.getClass();
        if (objClass.isArray()) {
            if (this.useAdaptersForContainers) {
                return DefaultArrayAdapter.adapt(obj, this);
            }

            obj = this.convertArray(obj);
        }

        if (obj instanceof Collection) {
            if (this.useAdaptersForContainers) {
                if (obj instanceof List) {
                    return DefaultListAdapter.adapt((List)obj, this);
                } else {
                    return (TemplateModel)(this.forceLegacyNonListCollections ? new SimpleSequence((Collection)obj, this) : DefaultNonListCollectionAdapter.adapt((Collection)obj, this));
                }
            } else {
                return new SimpleSequence((Collection)obj, this);
            }
        } else if (obj instanceof Map) {
            return (TemplateModel)(this.useAdaptersForContainers ? DefaultMapAdapter.adapt((Map)obj, this) : new SimpleHash((Map)obj, this));
        } else if (obj instanceof Boolean) {
            return obj.equals(Boolean.TRUE) ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE;
        } else if (obj instanceof Iterator) {
            return (TemplateModel)(this.useAdaptersForContainers ? DefaultIteratorAdapter.adapt((Iterator)obj, this) : new SimpleCollection((Iterator)obj, this));
        } else if (this.useAdapterForEnumerations && obj instanceof Enumeration) {
            return DefaultEnumerationAdapter.adapt((Enumeration)obj, this);
        } else {
            return (TemplateModel)(this.iterableSupport && obj instanceof Iterable ? DefaultIterableAdapter.adapt((Iterable)obj, this) : this.handleUnknownType(obj));
        }
    }
}

사용자로부터 자바 객체를 입력받은 다음 타입을 판단하여 TemplateModel 인터페이스의 구현체(Adapter 클래스)들을 리턴해준다. Map이나 Collection, Date 등의 객체를 해석할 수 있는 Adapter 클래스를 기본적으로 제공하고 있다.

DefualtObjectWrapper에서 해석 할 수 없는 자바 객체의 경우 'handleUnknownType()' 메소드가 호출된다. 간단하게 ObjectWrapper 클래스를 구현하고 싶은 경우 DefaultObjectWrapper 클래스를 확장(extends)해서 handleUnknownType() 메소드를 오버라이드해서 쓰면 된다.

ObjectWrapper 클래스가 리턴할 수 있는 TemplateModel 인터페이스의 서브 타입에는 어떤 것들이 있는지 알아보자.

TemplateModel 인터페이스 - Scalars

Freemarker에는 4가지 스칼라(Scalar) 타입이 있다.

  • Boolean
  • Number
  • String
  • Date-like (Subtypes : date, time, date-time)

각 스칼라 타입은 TemplateBooleanModel, TemplateNumberModel, TemplateStringModel, TemplateDateModel 인터페이스에 대응된다. 이 인터페이스들은타입에 따라서 get 메소드를 하나씩 갖는다. 예를 들어 TemplateNumberModel 인터페이스는 getAsNumber() 메소드를 갖는다. (각 타입에 대해서 getAs타입() 메소드를 갖는다)

이 인터페이스들의 가장 간단한 구현체는 freemarker.template 패키지의 SimpleNumber, SimpleString 등이 있다. Boolean을 위한 구현체로는 'SimpleBooleanModel.TRUE', 'SimpleBooleanModel.FALSE'라는 싱글턴 클래스가 존재한다.

이 구현체들은 템플릿에서 인터폴레이션(Interpolation) 자리에 해당 타입으로 치환된다. 예를 들어 '${a}'라는 인터폴레이션이 있을 때, 데이터 모델에서 a라는 경로의 TemplateModel이 SimpleNumber라면 '${a}' 인터폴레이션은 Number 타입의 값으로 치환된다. 만약 '${a}'가 SimpleString이라면 String 타입의 값이 치환된다.

스칼라 타입은 데이터 모델에서 리프 노드로 생각하면 된다.

TemplateModel 인터페이스 - Containers

Freemarker의 컨테이너(Containers) 타입에는 Hash, Sequence, Collectiond이 있다.

Hash

Freemarker의 Hash는 데이터 모델의 브랜치 노드라고 생각하면 된다. 자바의 HashMap 처럼 키 값을 이용해서 Value 값을 가져온다. 트리의 경우 현재 노드의 자식 노드의 이름을 입력해서 자식 노드를 가리키는 TemplateModel 객체를 받아오는 것을 생각하면 된다. (받아온 자식 TemplateModel은 또 다른 Hash 일 수도 있고, 스칼라 타입일 수도 있다.)

Hash 타입을 사용하기 위해서는 TemplateHashModel 인터페이스를 구현해야 한다. TemplateHashModel 인터페이스는 두 개의 메소드를 가지고 있다.

TemplateModel get(String key);

boolea isEmpty();

get() 메소드는 현재 노드의 자식으로 탐색할 노드 이름을 파라미터로 받는다. 파라미터로 입력받은 문자열에 해당하는 자식 노드로 사용할 TemplateModel 객체를 리턴한다.

isEmpty() 메소드는 자식이 없는 노드인지를 확인하는 메소드다.

Sequence

Freemarker의 Sequence 타입은 자바의 배열과 비슷하다. 배열에서 인덱스를 통해 특정 엘리먼트를 참조하는 것처럼 Sequence 타입 역시 인덱스를 통해 특정 엘리먼트를 참조할 수 있다.

Sequence 타입을 사용하기 위해서는 TemplateSequenceModel 인터페이스를 구현해야 한다. TemplateSequenceModel 인터페이스는 두 개의 메소드를 가지고 있다.

TemplateModel get(int index)

int size()

get() 메소드는 index번째 자식을 리턴해주는 메소드이고, size() 메소드는 전체 자식의 개수를 리턴해주는 메소드다.

'${a[2]}'라는 인터폴레이션이 템플릿에 있다면, get(2) 메소드가 수행된 것처럼 동작한다. 또 한, Sequence 타입은 템플릿 언어의 <#list> 디렉티브를 이용해서 자식 노드들을 순회할 수도 있다.

Collections

Freemarker의 Collection 타입은 TemplateCollectionModel 인터페이스를 구현하는 클래스이다. 프리마커의 Collection 타입은 자바의 컬렉션과 비슷하다.

TemplateModelIterator iterator() throws TemplateModelException;

자바의 컬렉션처럼 iterator() 메소드를 통해 iterator를 얻어온다. iterator() 메소드가 리턴하는 객체는 TemplateModelIterator 인터페이스를 구현한 객체다.

TemplateModelIterator 인터페이스는 두 개의 메소드로 구성되어 있다.

TemplateModel next() throws TemplateModelException;

boolean hasNext() throws TemplateModelException;

자바 컬렉션의 Iterator 구현과 비슷하다. TemplateCollectionModel를 구현한 Collections 타입은 Sequence 타입처럼 <#list> 디렉티브를 이용해서 자식들을 순회할 수 있다.

이처럼 컨테이너 타입과 스칼라 타입의 TemplateModel을 이용해서 자바 객체를 트리 형태의 데이터 모델로 해석할 수 있다.

예제

앞에서 살펴본 인터페이스들을 이용해서 자바 객체를 트리 형태로 해석해주는 ObjectWrapper 인터페이스와 Adapter 인터페이스들을 구현한 클래스를 만들어 보자.

우선 다음과 같은 자바 객체가 있다고 하자.

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class TestJavaObject {

    private String name;

    private List<String> list = new ArrayList<>();

    private Map<String, String> hash = new HashMap<>();

    public TestJavaObject(String name, List<String> list, Map<String, String> hash) {

        this.name = name;
        this.list = list;
        this.hash = hash;
    }

    public String getName() {

        return name;
    }

    public List<String> getList() {

        return list;
    }

    public Map<String, String> getHash() {

        return hash;
    }
}

이 객체를 다음과 같은 트리 형태로 모델링해보자.

TestJavaObject 객체를 위한 ObjectWrapper 클래스는 다음과 같이 작성할 수 있다.

import freemarker.template.DefaultObjectWrapper;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
import freemarker.template.Version;

public class TestObjectWrapper extends DefaultObjectWrapper {

    public TestObjectWrapper(Version incompatibleImprovements) {
        super(incompatibleImprovements);
    }

    @Override
    protected TemplateModel handleUnknownType(Object obj) throws TemplateModelException {

        if (obj instanceof TestJavaObject) {
            return new TestObjectAdapter(this, (TestJavaObject)obj);
        }

        return super.handleUnknownType(obj);
    }
}

TestObjectWrapper 클래스는 단순히 TestJavaObject를 받아서 TestObjectAdapter 클래스를 만들어서 리턴해준다. TestJavaObjectAdapter 클래스는 다음과 같이 작성할 수 있다.

import freemarker.template.ObjectWrapper;
import freemarker.template.TemplateHashModel;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
import freemarker.template.WrappingTemplateModel;

public class TestObjectAdapter extends WrappingTemplateModel implements TemplateHashModel {

    private final TestJavaObject object;

    public TestObjectAdapter(ObjectWrapper objectWrapper, TestJavaObject object) {

        super(objectWrapper);
        this.object = object;
    }

    @Override
    public TemplateModel get(String key) throws TemplateModelException {

        if ("objectName".equals(key))
            return wrap(object.getName());
        else if ("objectList".equals(key))
            return wrap(object.getList());
        else if ("objectMap".equals(key))
            return wrap(object.getHash());
        else
            return null;
    }

    @Override
    public boolean isEmpty() throws TemplateModelException {

        return false;
    }
}

TemplateHashModel을 구현한 TestObjectAdapter 클래스가 데이터 모델의 루트 노드 역할을 한다. 이 루트 노드가 가지고 있는 자식 노드는 각각 "objectName", "objectList", "objectMap"라는 이름을 가지고 있다.

"objectName"이라는 이름의 자식 노드는 TestJavaObject 클래스의 getName() 메소드를 호출해서 얻은 String 객체를 wrapping한 TemplateModel 클래스를 리턴한다. wrap() 메소드는 DefaultObjectWrapper 클래스의 wrap() 메소드에 해당하며, DefaultOjectWrapper 클래스의 wrap() 메소드는 String 타입에 대해서 SimpleScalar 객체를 리턴한다.

"objectList", "objectMap"의 경우도 동일하게 적당한 객체를 리턴한다.

이 클래스들을 사용하기 위한 메인 클래스는 다음과 같다.

import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import freemarker.cache.FileTemplateLoader;
import freemarker.cache.StringTemplateLoader;
import freemarker.template.Configuration;
import freemarker.template.Template;

public class TestFreemarker {

    public static TestJavaObject getTestObject() {

        List<String> list = new ArrayList<>();
        Map<String, String> map = new HashMap<>();

        list.add("List Value1");
        list.add("List Value2");
        list.add("List Value3");

        map.put("Key1", "Value1");
        map.put("Key2", "Value2");
        map.put("Key3", "Value3");
        map.put("Key4", "Value4");
        map.put("Key5", "Value5");

        return new TestJavaObject("TestObject", list, map);
    }

    public static void main(String []args) throws Exception {

        TestJavaObject object = getTestObject();

        StringTemplateLoader loader = new StringTemplateLoader();
        String nameTemplate = "${objectName}\n";
        String listTemplate = "${objectList[1]}\n";
        String mapTemplate = "${objectMap.Key1}\n";

        loader.putTemplate("nameTemplate", nameTemplate);
        loader.putTemplate("listTemplate", listTemplate);
        loader.putTemplate("mapTemplate", mapTemplate);

        Configuration cfg = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
        cfg.setTemplateLoader(loader);

        cfg.setObjectWrapper(new TestObjectWrapper(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS));

        PrintWriter writer = new PrintWriter(System.out);
        Template template = cfg.getTemplate("nameTemplate");
        template.process(object, writer);

        template = cfg.getTemplate("listTemplate");
        template.process(object, writer);

        template = cfg.getTemplate("mapTemplate");
        template.process(object, writer);
    }
}

이 클래스를 실행시켜보면, 다음과 같은 출력을 얻을 수 있다.

TestObject
List Value2
Value1

데이터 모델 트리에서 objectName에 해당하는 갑소가, objectList 중 인덱스가 1인 값, objectMap 중 키가 Key1인 값을 뽑아오는 예제다.

이런식으로 TemplateModel 들을 잘 엮어주는 Adapter 클래스를 만들어 나가면서 데이터모델을 트리형태로 만들 수 있다.

반응형

댓글