목공책 하나 들이셔요~

2014년 10월 6일 월요일

Java Scripting API 둘러보기 #3

이 글은 Java 6를 기준으로 쓰여진 Jeff Friesen의 "Taming Mustang, Part 2: Scripting API Tour"를 번역하고 Java 8에 맞게 수정한 것입니다. 원문은 다음을 참고하세요.

http://www.informit.com/articles/article.aspx?p=696621

바인딩과 스코프 이해하기

ScriptEngine의 put()과 get()함수를 이용하여 객체의 상태를 저장하고 가져오는 이면에는 Bindings 인터페이스를 구현하고 있는 바인딩 객체가 역할을 하고 있습니다. Scripting API에는 Bindings 인터페이스를 구현한 SimpleBindings 클래스도 제공되고 있습니다. Bindings 인터페이스는 java.util.Map 인터페이스를 확장(extend)한 것이라 맵과 같이 스트링 키와 그와 연결된 객체값들이 구조화되어 있습니다.




바인딩 객체는 스코프(scope)와 관련이 있습니다. 이 스코프는 바인딩 객체의 키/값 (key/value) 항목들이 해당 위치에서 보이느냐 안보이느냐(visibility)를 의미합니다. Scripting API는 글로벌 스코프와 엔진 스코프를 정의하고 있습니다.
  • 글로벌 스코프에 있는 키/값 항목들은 모든 스크립트 엔진들에서 보입니다. 
  • 엔진 스코프에 있는 키/값 항목들은 바인딩 객체가 관련된 그 엔진에서만 보입니다.
ScriptEngineManager 클래스는 글로벌 바인딩을 만들고 초기화합니다. 그리고 ScriptEngineManager는 setBindings(Bindings bindings) 메쏘드를 제공하여서 글로벌 바인딩을 교체할 수 있게 합니다. getBindings() 메쏘드는 글로벌 바인딩을 얻을 수 있게 합니다. 더 나아가 ScriptEngineManager는 put()과 get() 메쏘드를 제공하여 글로벌 스코프로 객체를 저장하고 가져오는 기능을 제공합니다. 정리하면 ScriptEngineManager에서 제공하는 바인딩과 관련된 메쏘드들은 모두 글로벌 스코프가 적용됩니다.

한편 ScriptEngine에서 제공하는 get()과 put() 함수에서도 스코프를 지정할 수 있습니다. 예를 들어 ScriptEngine이 제공하는 setBindings(Bindings bindings, int scope) 메쏘드는 scope인자의 값에 따라 엔진 스코프 혹은 글로벌 스코프의 바인딩을 설정할 수 있습니다. scope 인자는 ScriptContext.ENGINE_SCOPE 나 ScriptContext.GLOBAL_SCOPE 값을 가질 수 있습니다. 더불어 ScriptEngine은 getBindings(int scope) 메쏘드도 제공하여 scope에서 지정한 스코프의 바인딩 객체를 가져올 수 있게 합니다.

실제로 글로벌 스코프에서의 바인딩을 변경하는 것은 ScriptEngineManager의 getEngineByExtension(), getEngineByMimeType(), getEngineByName() 등의 메쏘드에서 새로 얻어진 스크립트 엔진의 글로벌 바인딩을 얻거나 설정하기 위해서 주로 사용됩니다.

스크립트를 실행하기 전에 엔진의 현재 바인딩을 바꿀 수 있습니다. 그리고 실행 후에 다시 원래의 바인딩으로 돌려놓는 것도 가능합니다. 스크립트 실행으로 인해 변경될 수 있는 바인딩 객체들을 원래대로 돌려놓고 항상 동일한 조건으로 스크립트를 돌려야 할 경우 유용합니다. 다음과 같이 하면 됩니다.
  1. ScriptEngine의 getBindings(int scope) 메쏘드를 ENGINE_SCOPE를 인자로 주고 실행하여 엔진의 현재 바인딩을 얻습니다. 그리고 이를 다른 변수에 할당합니다. 
  2. ScriptEngine의 createBindings() 메쏘드를 호출하여 새로운 바인딩 객체를 만듭니다. 
  3. ScriptEngine의 setBindings(Bindings bindings, int scope) 메쏘드를 ENGINE_SCOPE로 실행하여 새로 만든 바인딩을 엔진의 바인딩으로 설정합니다. 
  4. 원하는 대로 바인딩의 키/값 항목들을 설정하고 스크립트를 실행(eval)합니다. 
  5. 스크립트 실행이 완료되면 앞서 저장해 두었던 바인딩을 setBindings() 메쏘드를 이용하여 원상복구합니다. 
아래 리스트는 이런 스크립트 엔진 바인딩을 바꿔치기 하는 예를 보여줍니다.

// ScriptDemo4.java

import javax.script.*;

public class ScriptDemo4 {
    public static void main(String[] args) throws ScriptException {
      // Create a ScriptEngineManager that discovers all script engine
        // factories (and their associated script engines) that are visible to
        // the current thread's classloader.

        ScriptEngineManager manager = new ScriptEngineManager();

      // Obtain a ScriptEngine that supports the JavaScript short name.
        ScriptEngine engine = manager.getEngineByName("JavaScript");

      // Initialize the color and shape script variables.
        engine.put("color", "red");
        engine.put("shape", "rectangle");

      // Evaluate a script that outputs the values of these variables.
        engine.eval("print (color); print (shape);");

      // Save the current bindings object.
        Bindings oldBindings = engine.getBindings(ScriptContext.ENGINE_SCOPE);

      // Replace the bindings with a new bindings that overrides color and
        // shape.
        Bindings newBindings = engine.createBindings();
        newBindings.put("color", "blue");
        engine.setBindings(newBindings, ScriptContext.ENGINE_SCOPE);
        engine.put("shape", "triangle");

      // Evaluate the script.
        engine.eval("print (color); print (shape);");

      // Restore the original bindings.
        engine.setBindings(oldBindings, ScriptContext.ENGINE_SCOPE);

      // Evaluate the script.
        engine.eval("print (color); print (shape);");
    }
}

위 소스코드를 보면 키/값 항목은 ScriptEngine의 put() 메쏘드를 통해서도, Bindings의 put() 메쏘드를 통해서도 등록할 수 있음을 알 수 있습니다. 위 ScriptDemo4는 다음과 같은 결과를 보여줍니다.

red
rectangle
blue
triangle
red
rectangle

만일 스크립트를 실행한 후 바인딩에 등록된 키/값 상태가 변치않는 걸 원한다면 ScriptEngine의 eval(String script, Bindings bindings) 메쏘드나 eval(Reader reader, Bindings bindings) 메쏘드를 사용하는게 편할 수 있습니다. 이 메쏘드들은 인자로 주어진 바인딩 객체들을 사용하며 엔진의 현재 바인딩 객체의 상태는 변경하지 않습니다.

스크립트 컨텍스트 이해하기

ScriptEngine의 setBindings()와 getBindings() 메쏘드는 사실 ScriptContext 인터페이스의 setBindings()와 getBindings() 메쏘드를 쉽게 사용하기 위해 만들어진 것들입니다.   ScriptContext는 스크립트의 컨텍스트(문맥)을 나타내기 위한 것으로 Java 프로그램을 스크립트 엔진과 연결하는 역할을 합니다.  SimpleScriptContext 클래스는 ScriptContext 인터페이스를 구현한 것으로 기본 제공됩니다. 

기본적으로 스크립트 컨텍스트는 엔진과 그와 연결된 글로벌 바인딩을 스크립트 엔진에 노출시키는 역할을 합니다. 이렇게 함으로서 Java 프로그램은 스크립트 변수를 통해 스크립트와 통신을 할 수 있습니다. 또한 스크립트 컨텍스트는 스크립트 엔진의 입출력을 위해 java.io.Reader 하나와 두개의 java.io.Writer를 노출합니다.

스크립트 엔진의 바인딩은 디폴트로 비어 있습니다. 또한 스크립트 엔진의 디폴트 입력은 java.io.InputStreamReader 타입의 System.in 객체이며, 디폴트 출력은 java.io.PrintWriter 타입으로 System.out과 System.err 객체입니다. 즉 표준 입력과 표준 출력, 표준 에러 출력 장치입니다.

ScriptContext의 setReader(Reader reader) 메쏘드를 이용하여 스크립트 엔진의 입력을 바꿀 수 있습니다. getReader() 메쏘드를 이용하면 현재의 입력 리더를 얻을 수도 있습니다. 비슷하게 setWriter(Writer writer) 메쏘드와 setErrorWriter(Writer writer) 메쏘드를 사용하면 스크립트 엔진의 출력과 에러 출력을 변경할 수 있습니다. 마찬가지로 getWriter와 getErrorWriter() 메쏘드를 이용하면 현재의 출력 Writer를 얻을 수 있습니다.

다음 소스코드는 setWriter() 메쏘드를 이용하여 스크립트 엔진의 출력을 표준 콘솔 출력 대신에 스트링으로 출력하도록 하는 예입니다.

// ScriptDemo5.java

import java.io.*;
import javax.script.*;

public class ScriptDemo5 {
    public static void main(String[] args) throws ScriptException {
      // Create a ScriptEngineManager that discovers all script engine
        // factories (and their associated script engines) that are visible to
        // the current thread's classloader.

        ScriptEngineManager manager = new ScriptEngineManager();

      // Obtain a ScriptEngine that supports the JavaScript short name.
        ScriptEngine engine = manager.getEngineByName("JavaScript");

      // Redirect the engine's standard output to a StringWriter instance.
        StringWriter writer = new StringWriter();
        PrintWriter pw = new PrintWriter(writer, true);
        engine.getContext().setWriter(pw);

      // Evaluate a script that outputs some text.
        engine.eval("print ('This output will go to the string writer.');");

      // Obtain the string buffer's contents and output these contents.
        StringBuffer sb = writer.getBuffer();
        System.out.println("StringBuffer contains: " + sb);
    }
}

setWriter()와 setErrorWriter()의 인자로 어떤 Writer도 전달이 가능하지만, Rhino Java Script 엔진의 경우 반드시 PrintWriter여야 하니 주의 바랍니다.

위 코드는 다음과 같이 출력됩니다.

StringBuffer contains: This output will go to the string writer.

ScriptEngine은 setContext()와 getContext() 메쏘드를 가지고 있어서 스크립트 실행 전에 현재의 컨텍스트를 저장해 두었다가 스크립트 실행 후 원상복귀할 수 있습니다. 하지만 컨텍스트의 보존을 위해 일일이 저장하고 복원하는 절차를 회피하려면 ScriptEngine의 eval() 메쏘드 중에서 ScriptContext를 인자로 받는 eval(String script, ScriptContext context)와 eval(Reader reader, ScriptContext context) 메쏘드들을 사용하면 스크립트를 실행하는 동안 인자로 주어진 컨텍스트를 사용하므로 엔진에 연결된 스크립트 컨텍스트는 자연스럽게 보존됩니다.

댓글 없음:

댓글 쓰기