오늘날 많은 Java 개발자가 Java 플랫폼에서 스크립팅 언어를 사용하는 것에 관심을 가지고 있지만 Java 바이트코드로 컴파일된 동적 언어를 사용하는 것이 항상 가능하지는 않다. 어떤 경우에는 Java 애플리케이션의 일부만 스크립트로 작성하거나 스크립트 내에서 필요한 특정 Java 오브젝트를 호출하는 것이 더 빠르고 효율적이다.
바로 그 때문에 javax.script가 필요하다. Java 6에 도입된
Java Scripting API는 사용하기 편리한 간단한 스크립팅 언어와 강력한 Java 에코시스템을
연결해 준다. Java Scripting API를 사용하면 거의 모든 스크립팅 언어를 Java 코드와 빠르게
통합할 수 있다. 따라서 간단한 문제점을 해결할 때 상당히 다양한 방법을 활용할 수 있다.
1. jrunscript로 JavaScript 실행하기
각각의 새 Java 플랫폼 릴리스는 JDK의 bin 디렉토리 내에 새 명령행 도구 세트를 포함하고
있다. Java 6도 예외가 아니며 jrunscript는 중요한 Java 플랫폼
유틸리티 중 하나이다.
성능 모니터링을 위한 명령행 스크립트를 작성할 때 발생할 수 있는 기본적인 문제점을
살펴보자. 이 도구에서는 프로세스의 실행 상태를 확인하기 위해 이 시리즈의 이전
기사에서 설명한 jmap을 Java 프로세스에 대해 5초마다
실행한다. 일반적으로 명령행 쉘 스크립트만으로도 충분하겠지만 이 경우에는 서버 애플리케이션을
Windows®와 Linux®를 포함한 다양한 플랫폼에 전개한다. 시스템 관리자에게 물어보면
두 플랫폼에서 모두 실행되는 쉘 스크립트를 작성하는 것이 얼마나 어려운 일인지 잘 알 수 있을
것이다. 일반적으로 Windows 일괄처리 파일과 UNIX® 쉘 스크립트를 작성한 후 두 파일의
동기화를 지속적으로 유지하는 방법이 주로 사용된다.
하지만 The Pragmatic Programmer를 읽어본 독자라면 알겠지만 이 방법은 DRY(Don't Repeat Yourself) 원칙을 심각하게 위반할 뿐만 아니라 버그 및 결함의 배양소가 될 수 있다. 우리가 진정으로 원하는 바는 모든 플랫폼에서 실행할 수 있는 일종의 OS 중립적 스크립트를 작성하는 것이다.
물론 Java 언어가 플랫폼 중립적이기는 하지만 지금 여기에서 다루고자 하는 바는 "시스템" 언어가 아니다. 우리에게 필요한 것은 JavaScript와 같은 스크립팅 언어이다.
먼저 Listing 1에서는 이 기사에서 설명하려는 기능의 기본 쉘을 보여 준다.
Listing 1. periodic.js
while (true)
{
echo("Hello, world!");
} |
웹 브라우저와의 상호 작용이 중요해 지면서 대부분은 아닐지라도 많은 Java 개발자가 JavaScript(또는 ECMAScript)를 이미 알고 있다. (JavaScript는 Netscape 소유 ECMAScript의 또 다른 이름이다.) 따라서 이제는 시스템 관리자가 이 스크립트를 어떻게 실행할 수 있는가라는 질문에 대한 답을 찾을 차례이다.
물론 그 답은 JDK에 포함된 jrunscript 유틸리티이다(Listing 2 참조).
Listing 2. jrunscript
C:\developerWorks\5things-scripting\code\jssrc>jrunscript periodic.js Hello, world! Hello, world! Hello, world! Hello, world! Hello, world! Hello, world! Hello, world! ... |
for 루프를 사용해서도 종료하기 전까지 지정된 횟수
동안 스크립트를 실행할 수 있다. 기본적으로 jrunscript를
사용하면 JavaScript로 수행할 수 있는 거의 모든 작업을 수행할 수 있다. 유일한 예외는
환경이 브라우저가 아니기 때문에 DOM이 없다는 것이다. 따라서 사용할 수 있는 최상위
함수 및 오브젝트가 약간 다르다.
Java 6에는 Rhino ECMAScript 엔진이 JDK의 일부로 포함되어 있기 때문에 jrunscript가
이 기사에서 보듯이 파일로부터 제공되거나 REPL("Read-Evaluate-Print-Loop")이라는 대화식 쉘 환경에서 제공되는
ECMAScript 코드를 실행할 수 있다. jrunscript를 실행하여 REPL 쉘에 액세스할 수 있다.
JavaScript/ECMAScript 코드를 작성하는 것도 좋지만 이 기사에서는 Java 언어에서 사용하는 모든 기능을 처음부터
다시 빌드하는 것을 원하지 않는다. 왜냐하면 목적에 부합하지 않기 때문이다. 다행스럽게도 Java Scripting API 엔진을
사용하는 기능은 본질적으로 Java 바이트코드이므로 전체 Java 에코시스템에 액세스할 수 있다. 따라서 앞의 문제점을
다시 생각해 보면, 일반적인 Runtime.exec() 호출을 사용하여 Java 플랫폼의 프로세스를
실행할 수 있다(Listing 3 참조).
Listing 3. Runtime.exec()로 jmap 실행하기
var p = java.lang.Runtime.getRuntime().exec("jmap", [ "-histo", arguments[0] ])
p.waitFor()
|
arguments 배열은 이 함수에 전달되는 인수에 대한 ECMAScript
표준 내장 참조이다. 최상위 스크립트 환경의 경우 이 배열은 스크립트 자체에 전달되는 인수
배열(명령행 매개변수)이다. 따라서 Listing 3의 스크립트는 맵핑할 Java 프로세스의 VMID가
포함된 인수를 기대한다.
또는 jmap이 Java 클래스라는 점을 감안하여 이 클래스의
main() 메소드를 호출할 수도 있다(Listing 4 참조). 이 방법을
사용하면 Process 오브젝트의 in/out/err
스트림을 "파이프로 연결"하지 않아도 된다.
Listing 4. JMap.main()
var args = [ "-histo", arguments[0] ] Packages.sun.tools.jmap.JMap.main(args) |
Packages 구문은 Rhino 내에 이미 설정되어 있는 핵심 java.*
패키지에 포함되지 않은 Java 패키지를 참조하는 데 사용되는 Rhino ECMAScript 표기법이다.
스크립트에서 Java 오브젝트를 호출하는 것은 단지 반쪽에 불과하다. Java 스크립팅 환경은 Java 코드
내에서 스크립트를 호출하는 기능도 제공한다. 이를 위해서는 ScriptEngine을
인스턴스화한 후 스크립트를 로드하고 평가하기만 하면 된다(Listing 5 참조).
Listing 5. Java 플랫폼에서의 스크립팅
import java.io.*;
import javax.script.*;
public class App
{
public static void main(String[] args)
{
try
{
ScriptEngine engine =
new ScriptEngineManager().getEngineByName("javascript");
for (String arg : args)
{
FileReader fr = new FileReader(arg);
engine.eval(fr);
}
}
catch(IOException ioEx)
{
ioEx.printStackTrace();
}
catch(ScriptException scrEx)
{
scrEx.printStackTrace();
}
}
} |
eval() 메소드는 String에 대해서도
작동하므로 스크립트를 파일 시스템에 있는 파일에서 가져오지 않아도 되며 데이터베이스나 사용자로부터
가져오거나 애플리케이션 내에서 환경 및 사용자 조치를 기반으로 작성할 수도 있다.
스크립트를 호출하는 것만으로는 충분하지 않다. 스크립트에서 Java 환경 내에서 작성된 오브젝트와
상호 작용해야 하는 경우도 있다. 이러한 경우 Java 호스트 환경에서는 오브젝트를 작성하고 바인드해야
한다. 이렇게 하면 스크립트에서 오브젝트를 쉽게 찾아서 사용할 수 있다. 이는 ScriptContext
오브젝트에 대한 태스크이다(Listing 6 참조).
Listing 6. 스크립트에 대한 오브젝트 바인드하기
import java.io.*;
import javax.script.*;
public class App
{
public static void main(String[] args)
{
try
{
ScriptEngine engine =
new ScriptEngineManager().getEngineByName("javascript");
for (String arg : args)
{
Bindings bindings = new SimpleBindings();
bindings.put("author", new Person("Ted", "Neward", 39));
bindings.put("title", "5 Things You Didn't Know");
FileReader fr = new FileReader(arg);
engine.eval(fr, bindings);
}
}
catch(IOException ioEx)
{
ioEx.printStackTrace();
}
catch(ScriptException scrEx)
{
scrEx.printStackTrace();
}
}
} |
바인드된 오브젝트에 액세스하는 방법은 간단하다. 바인드된 오브젝트의 이름이 스크립트 레벨에서
글로벌 네임스페이스의 멤버로 소개되므로 Listing 7과 같이 손쉽게 Rhino의 Person
오브젝트를 사용할 수 있다.
Listing 7. 이 기사의 필자는 누구인가?
println("Hello from inside scripting!")
println("author.firstName = " + author.firstName)
|
위 Listing에서 보듯이 JavaBeans 스타일 특성은 마치 필드처럼 직접적인 이름 액세서로 축소된다.
스크립팅 언어의 단점은 항상 성능에 있다. 그 이유는 대부분의 스크립팅 언어가 "즉석에서" 해석되고 실행될 때마다 구문 분석 및 유효성 검증을 수행하기 위해 시간과 CPU 주기를 소비해야 하기 때문이다. JVM에서 실행되는 많은 스크립팅 언어는 적어도 스크립트가 처음으로 구문 분석 및 유효성 검증될 때 수신 코드에서 Java 바이트코드로 변환되기 마련이며, 이 즉석 컴파일은 Java 프로그램이 종료될 때 삭제된다. 자주 사용하는 스크립트를 바이트코드 양식으로 유지하면 상당한 성능 향상 효과를 얻을 수 있다.
Java Scripting API를 사용하여 자연스럽고 의미 있는 방식으로 이 작업을 수행할 수 있다. 리턴된
ScriptEngine에 Compilable 인터페이스가
구현되어 있다면 이 인터페이스의 compile 메소드를 사용하여 스크립트(String
또는 Reader로 전달됨)를 CompiledScript
인스턴스로 컴파일할 수 있다. 그런 다음 이 인스턴스를 사용하여 다양한 바인딩을 가지고 반복적으로
컴파일된 코드에 대한 eval()을 수행할 수 있다. Listing 8에서 이 작업을 보여 준다.
Listing 8. 해석된 코드 컴파일하기
import java.io.*;
import javax.script.*;
public class App
{
public static void main(String[] args)
{
try
{
ScriptEngine engine =
new ScriptEngineManager().getEngineByName("javascript");
for (String arg : args)
{
Bindings bindings = new SimpleBindings();
bindings.put("author", new Person("Ted", "Neward", 39));
bindings.put("title", "5 Things You Didn't Know");
FileReader fr = new FileReader(arg);
if (engine instanceof Compilable)
{
System.out.println("Compiling....");
Compilable compEngine = (Compilable)engine;
CompiledScript cs = compEngine.compile(fr);
cs.eval(bindings);
}
else
engine.eval(fr, bindings);
}
}
catch(IOException ioEx)
{
ioEx.printStackTrace();
}
catch(ScriptException scrEx)
{
scrEx.printStackTrace();
}
}
} |
대부분의 경우 동일한 스크립트를 반복해서 다시 컴파일하지 않으려면 CompiledScript
인스턴스를 장기 저장소(예: servlet-context)에 저장해야 한다. 하지만 스크립트 내용을
변경해야 하는 경우에는 새 CompiledScript를 작성하여 변경을 반영해야 한다. 컴파일된
CompiledScript는 더 이상 원본 스크립트 파일의 내용을 실행하지 않는다.
Java Scripting API는 Java 프로그램의 지평과 기능을 넓힐 수 있는 중요한 도약대이며
Java 환경에서도 스크립팅 언어의 생산성을 얻을 수 있는 기회를 제공한다. 프로그램을 작성하는
것보다 훨씬 쉽게 사용할 수 있는 jrunscript와 함께 javax.script는
Java 개발자에게 Java 환경의 에코시스템과 확장성을 포기하지 않고도 Ruby(JRuby) 및 ECMAScript(Rhino)와
같은 스크립팅 언어를 활용할 수 있는 방법을 제공한다.
5가지 사항 시리즈의 다음 주제는 JDBC이다.
교육
- 모르고
있던 5가지 사항: Java 플랫폼에 대해 모르는 것이 많았다는 것을 일깨워 주는 이 시리즈에서는
사소하게 여겼던 Java 기술을 유용한 프로그래밍 팁으로 바꿔준다.
- "동적인
언어를 동적으로 호출하기, Part 1: Java Scripting API"(Tom McQueeney 저, developerWorks, 2007년 9월): 두 편의 기사로 이
시리즈의 Part 1에서는 Java Scripting API의 기능을 소개하고, Part 2에서는 이 API의 활용 방법을 자세히 설명한다.
- "JavaScript
EE, Part 3: Use Java scripting API with JSP"(Andrei Cioroianu 저, developerWorks, 2009년 6월): JavaScript와 Java 플랫폼을
결합하는 방법과 웹 브라우저에서 JavaScript가 사용되지 않을 때도 기능이 유지되는 Ajax 사용자 인터페이스를 빌드하는 방법에 대해 알아보자.
- JDK
Tools and Utilities: 성능 모니터링과 관련하여 5가지 사항에서 중점적으로 다루고 있는
jmap을 포함한 여러 가지 실험 수준의 모니터링 및 문제점 해결 도구에 대해 알아보자. - developerWorks Java 기술 영역: Java 프로그래밍과 관련된 모든 주제를 다루는 여러 편의 기사를 찾아보자.
토론
- My developerWorks 커뮤니티에 참여하자.
개발자가 이끌고 있는 블로그, 포럼, 그룹 및 Wiki를 살펴보면서 다른 developerWorks 사용자와 의견을 나눌 수 있다.