JVM에서 JavaScript 프로그램 하기(Nashorn에 대한 소개)

의역, 오역, 직역이 있을 수 있음을 알려드립니다.
이 포스트는 원저자의 동의를 얻어 한글로 번역한 내용입니다.

This post is a translation of this original article Riding the Nashorn: Programming JavaScript on the JVM by Niko Köbler from http://www.n-k.de
Original article: Riding the Nashorn: Programming JavaScript on the JVM

Author: Niko Köbler
Author Email: niko@n-k.de
Author Blog: http://www.n-k.de
Author Twitter: @dasniko


Nashorn은 Java8 버전 부터 Java Virtual Machine의 공식 JavaScript 엔진입니다.
ECMAScript 5.1 사양에 따라 구현되었으며 Google V8 (Node.js의 스크립트 엔진)과 경쟁합니다.
Nashorn은 런타임 동안 JavaScript를 Java 바이트 코드로 컴파일하기 때문에 Java와 JavaScript의 사이의 높은 상호 운용성을 제공합니다.

이 글은 Nashorn에서 가장 많이 사용 되는 Option, Function, Feature와 그리고 Use case에 대해 살펴 보려고합니다.
이 튜토리얼을 읽은 후에는 일반 Java 프로그램에서 Nashorn을 이용하여 JavaScript 동적 스크립팅 기능을 사용할 수 있을 것으로 기대합니다.

Nashorn을 공부하기 위한 또 다른 좋은 방법은 Official Project Nashorn 웹 사이트이며, 공식 웹사이트에는 메일 링리스트, 블로그 및 wiki 페이지들이 있습니다. 바로 확인해 보세요.

JDK 8의 Nashorn은 위에 언급 한대로 ECMAScript 5.1 사양을 구현했습니다.
Nashorn의 전략은 ECMAScript 사양을 따르는 것입니다. 그렇기 때문에 Nashorn의 주요 후속 버전에서는 ECMAScript 2015 spec을 구현할 예정입니다.

1. Nashorn의 Command Line Interface (CLI)

Nashorn은 jjs라는 명령행 클라이언트(CLI)를 제공합니다.

jjs는 $JAVA_HOME/bin에 위치해 있지만, 바로가기/링크/Path등의 설정이 Java 8 설치 기능에 포함되어 있지 않습니다. 따라서 jjs로 작업하기를 원한다면 아래와 같이 jjs 링크를 설정해야 합니다.

1
2
3
4
$ cd /usr/bin
$ ln -s $JAVA_HOME/bin/jjs jjs
$ jjs
jjs> print('Hello World');

환경변수의 PATH에 $JAVA_HOME/bin을 추가하면 따로 설정할 필요가 없습니다.

이제 jjs 쉘에서 직접 JavaScript 코드를 실행할 수 있습니다.

1
2
3
jjs> var x = 10;
jjs> print(x);
10

그리고 jjs를 이용하여 파일로 저장되어 있는 JavaScript를 로드하고 실행할 수 있습니다.

test.js

1
2
3
var x = 10;
var y = 20;
print(x + y);

1
2
$ jjs test.js
30
jjs 종료

jjs 클라이언트는 아래와 같은 함수를 호출하여 종료할 수 있습니다.

1
2
3
4
5
jjs> exit() // 일반적인 종료
jjs> exit(1) // 종료 코드를 포함한 종료
jjs> quit() // 일반적인 종료
jjs> quit(1) // 종료 코드를 포함한 종료
도움말 / 옵션

-help 옵션을 사용하면 jjs의 모든 옵션이 나열됩니다.

1
$ jjs -help

1.1. Scripting 모드

Nashorn은 ECMAScript 5.1 사양 외에도 자체 구문및 확장 API를 구현했습니다. (다음 장 참조).
이러한 확장 기능의 대부분은 스크립팅 모드에서만 사용할 수 있습니다.
스크립팅 모드는 cli의 -scripting 옵션을 사용하여 활성화 됩니다.

1
$ jjs -scripting

스크립팅 모드에서 Nashorn은 Shell 스타일의 ‘#’을 이용한 한 줄 전체 주석을 사용할 수 있습니다.

test.js

1
2
3
4
# style line comment -scripting mode
# prints hello
print('hello');

1.2 Shebang

Shebang은 Unix 계열 shell script에서 script 해석 인터프리터를 명시한 것을 말합니다.
대부분의 Unix 계열 shell script의 첫번째 라인에 아래와 같은 부분이 있는데 이게 Shebang입니다.

1
2
3
4
#!/usr/bin/perl
#!/bin/sh
#!/usr/bin/php
#!/usr/bin/ruby

Nashorn은 Shebang 스크립팅을 지원합니다.
따라서 아래와 같이 기존 방식처럼 JavaScript 파일을 호출하는 대신

test.js

1
print('hello');

1
$ jjs test.js

첫 줄에 Shebang과 함께 다음과 같은 JavaScript 파일을 작성할 수 있습니다.

1
2
#!/usr/bin/jjs
print('hello');

다른 shell 스크립트와 마찬가지로 실행 파일로 만들고 명령 행에서 실행할 수 있습니다.

1
2
$ chmod 755 test.js
$ ./test.js

이 장의 시작 부분에서 언급했듯이 jjs에 대한 링크가 있어야합니다.
Shebang Nashorn 스크립트를 실행하면 스크립팅 모드가 자동으로 활성화됩니다.

2. Java에서 Nashorn JavaScript 엔진 사용

Nashorn 엔진은 JSR-223(Scripting for the Java Platform)의 자바 스크립트 구현이며 javax.script API의 구현체 입니다.
따라서 Java에서 JavaScript 코드를 사용하기 위해서는 Nashorn javax.script.ScriptEngine을 생성 해야 합니다.

1
2
ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn"); // --- (1)
engine.eval("print('Hello World');");

(1) 여기에서 사용할 수있는 Nashorn 엔진 이름은 “nashorn”, “javascript”“js”입니다.

위에서 볼 수 있듯이 JavaScript 코드는 엔진 객체의 eval() 메서드에 문자열로 전달하여 직접 실행할 수 있습니다.
또는 파일을 가리키는 FileReader 객체를 전달하여 .js 파일을 구문 분석(및 실행)을 할 수 있습니다.

1
2
ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
engine.eval(new FileReader("test.js"));

2.1. Java에서 JavaScript 함수 호출

Java 프로그램 내부에서 단일 JavaScript 구문 또는 JavaScript 파일을 실행하거나 JavaScript 함수를 호출할 수도 있습니다.
또한 Java 객체를 JavaScript 함수의 arguments로 전달하고, JavaScript 함수에서 호출한 Java 메서드로 데이터를 반환할 수 있습니다.

example.js

1
2
3
4
var sayHello = function(name) {
print('Hello, ' + name + '!');
return 'hello from javascript';
};

위와 같이 작성된 example.js에 정의된 sayHello 함수를 호출하려면,
먼저 아래의 코드에서 보는 바와 같이 engine 객체를 NashornScriptEngine에서 구현된 Invocable Interface로 캐스팅해야합니다.
Invocable Interface는 invokeFunktion() 메서드를 제공하는데, 이 메서드는 입력값으로 받은 JavaScript 함수이름과 arguments를 이용해 Javascript를 실행할수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
engine.eval(new FileReader("example.js"));
// cast the script engine to an invocable instance
Invocable invocable = (Invocable) engine;
Object result = invocable.invokeFunction("sayHello", "John Doe");
System.out.println(result);
System.out.println(result.getClass());
// Hello, John Doe!
// hello from javascript
// class java.lang.String

위에 기술한 코드는 콘솔에 세 줄을 출력합니다.
JavaScript의 sayHello 함수의 print()System.out에 pipe되어 출력하고, Java 코드의 두개 System.out.println() 메소드에서 sayHello 함수의 리턴값과 리턴값의 Class이름을 출력합니다.

2.2. Javascript에서 Java method 호출

JavaScript 코드에서 Java 메소드를 호출하거나 실행하는 반대의 경우도 간단합니다.
Java의 두 종류의 메소드를 가정해 보겠습니다.

MyJavaClass.java

1
2
3
4
5
6
7
8
9
10
11
12
13
package my.package;
public class MyJavaClass {
public static String sayHello(String name) {
return String.format("Hello %s from Java!", name);
}
public int add(int a, int b) {
return a + b;
}
}

Java 클래스는 Java.type API 확장을 통해 JavaScript에서 참조할 수 있습니다.
이는 Java의 import 문과 유사합니다. Java 클래스를 참조한 후에 sayhello() 메서드와 같은 static method는 바로 호출하여 결과를 System.out에 출력할 수 있습니다. 또한 add()와 같은 일반 메서드들은 class의 인스턴스를 생성하여 호출할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
var MyJavaClass = Java.type('my.package.MyJavaClass');
// call the static method
var greetingResult = MyJavaClass.sayHello('John Doe');
print(greetingResult);
// create a new intance of MyJavaClass
var myClass = new MyJavaClass();
var calcResult = myClass.add(1, 2);
print(calcResult);
// Hello John Doe from Java!
// 3

2.2.1. Nashorn의 type 변환

아래의 간단한 예제를 통해 JavaScript에서 Java 메서드를 호출할 때 Nashorn이 Java와 JavaScript 간의 형식 변환을 처리하는 방법을 확인할 수 있습니다.

1
2
3
public static void printType(Object object) {
System.out.println(object.getClass());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
MyJavaClass.printType('Hello');
// class java.lang.String
MyJavaClass.printType(123);
// class java.lang.Integer
MyJavaClass.printType(12.34);
// class java.lang.Double
MyJavaClass.printType(true);
// class java.lang.Boolean
MyJavaClass.printType(new Number(123));
// class jdk.nashorn.internal.objects.NativeNumber
// class jdk.nashorn.api.scripting.ScriptObjectMirror
MyJavaClass.printType(new Date());
// class jdk.nashorn.internal.objects.NativeDate
// class jdk.nashorn.api.scripting.ScriptObjectMirror
MyJavaClass.printType(new RegExp());
// class jdk.nashorn.internal.objects.NativeRegExp
// class jdk.nashorn.api.scripting.ScriptObjectMirror
MyJavaClass.printType({foo: 'bar'});
// class jdk.nashorn.internal.scripts.J04
// class jdk.nashorn.api.scripting.ScriptObjectMirror
  • Primitive JavaScript type은 적절한 Java 래퍼 클래스로 변환됩니다.
  • Native JavaScript 객체는 ScriptObjectMirror 내부의 각각 어댑터 클래스로 표현됩니다.

jdk.nashorn.internal의 클래스는 추후 변경 될 수 있으므로 클라이언트 코드에서 해당 클래스에 대해 프로그램을 작성하면 안됩니다.

2.3. ScriptObjectMirror

ScriptObjectMirrorjdk.nashorn.api의 일부이며 기본 Javascript 내부 클래스 대신 Java 클라이언트 코드에서 사용하기위한 것입니다. 이 미러 객체는 JavaScript의 기본 객체의 표현입니다. 또한 Java Map 인터페이스를 구현하였으며, 객체, 메서드 및 속성에 대한 액세스를 제공합니다.

위의 예제를 조금 바꿔 보겠습니다.

1
2
3
public static void printObjectMirror(ScriptObjectMirror mirror) {
System.out.println(mirror.getClassName() + ": " + Arrays.toString(mirror.getOwnKeys(true)));
}

마지막 네개의 JavaScript 함수 호출 (number, date, regexp 및 object literal)을 사용하여이 메서드를 호출하면 다음과 같습니다.

1
2
3
4
5
6
7
MyJavaClass.printObjectMirror(new Number(123));
MyJavaClass.printObjectMirror(new Date());
MyJavaClass.printObjectMirror(new RegExp());
MyJavaClass.printObjectMirror({
foo: 'bar',
bar: 'foo'
});

결과는 다음과 같습니다.

1
2
3
4
Number: []
Date: []
RegExp: [lastIndex, source, global, ignoreCase, multiline]
Object: [foo, bar]

또한 Java에서 JavaScript 객체의 멤버 함수를 호출할 수 있습니다.
firstName, lastName 속성과 getFullName() 함수를 가진 JavaScript Person 타입을 가정해 보겠습니다.

1
2
3
4
5
6
7
function Person(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
this.getFullName = function() {
return this.firstName + ' ' + this.lastName;
}
}

Javascript의 getFullName() 함수는 ScriptObjectMirror 클래스의 callMember() 메서드를 이용해 호출할수 있습니다.

1
2
3
public static void getFullName(ScriptObjectMirror person) {
System.out.println("Full name is: " + person.callMember("getFullName"));
}

person 객체를 Java 메서드에 전달하면 콘솔에 원하는 결과가 표시됩니다.

1
2
3
4
var person = new Person('John', 'Doe');
MyJavaClass.getFullName(person);
// Full name is: John Doe

2.4. Script engine 옵션

nashorn.args system property을 사용하여 Nashorn 스크립트 엔진을 customize 할 수 있습니다. 예를 들어 아래와 같이 스트립팅 모드 사용옵션을 -Dnashorn.args = ...을 이용하여 지정할 수 있습니다.

1
$ java -Dnashorn.args=-scripting MyJavaClass

customize 옵션을 파라메터로 넘겨 Nashorn engine을 생성할 수도 있습니다. 이 경우 아래 예제와 같이 NashornScriptEngineFactory를 직접 인스턴스화 해야합니다.

1
2
NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
ScriptEngine engine = factory.getScriptEngine(new String[] { "-scripting" });

jjs -help를 호출하여 사용 가능한 옵션을 확인할 수 있습니다.

2.5. Bindings / Context

ScriptContext는 하나 이상의 jsr223 “scope”이 Binding 되어 포함되어 있습니다. 그리고 디폴트 scope으로 ENGINE_SCOPEGLOBAL_SCOPE의 두 가지가 있습니다.

  • ENGINE_SCOPE
  • GLOBAL_SCOPE

Nashorn 엔진이 생성되고 default context를 생성합니다.

1
ScriptContext defaultContext = engine.getContext();

default contextENGINE_SCOPE은 ECMAScript “global” object를 래핑한 인스턴스입니다. 이 object는 최상위 script의 “this”입니다. 따라서 이 scope 객체에서 “Object”, “Math”, “RegExp”, “undefined”와 같은 ECMAScript 최상위 객체를 액세스할 수 있습니다. 또한 GLOBAL_SCOPE은 같은 ScriptEngineManager로 작성된 모든 엔진 사이에 공유됩니다.
context에 변수들을 저장할 수 있지만 scope은 선택 사항이며 기본값은 ENGINE_SCOPE입니다.

1
2
3
4
5
6
7
8
9
ScriptContext context = engine.getContext();
// stores an object under the key `myKey` in the (engine scoped) context
context.setAttribute("myKey", object, ScriptContext.ENGINE_SCOPE);
// retrieves the object with key `myKey` from (engine scoped) context
context.getAttribute("myKey", ScriptContext.ENGINE_SCOPE);
Bindings b = context.getBindings(ScriptContext.ENGINE_SCOPE);
b.get("Object"); // gets ECMAScript "Object" constructor
b.get("undefined"); // ECMAScript 'undefined' value

Nashorn이 변수를 검색할 때 ENGINE_SCOPE에 없으면 GLOBAL_SCOPE Binding에서 검색합니다.

ECMAScript의 “global“ property가(최상위 레벨의 “this”) GLOBAL_SCOPE가 아닌 ENGINE_SCOPE에 있어 혼란이 있을 수 있습니다.

더 많은 Bindings와 ScriptContext정보는 wiki를 참고하세요.

3. API and Language Extensions

Nashorn은 ECMAScript 표준 외에도 다양한 언어 및 API 확장 기능을 제공합니다. 대부분의 API 확장은 Rhino에 대한 호환성 때문에 발생합니다.

Rhino는 mozilla에서 개발한 Java로 구현된 JavaScript engine이다.
과거 Netscape에서 Java로 구현된 navigator를 구현하려는 시도를 한 적이 있는데, 이때 사용했던 JavaScript engine이 Rhino engine의 전신이된다.
Javagator라고 불리던 이 프로젝트는 JavaScript를 Java byte code로 컴파일하여 실행하기 때문에 당시에 있던 다른 브라우저보다 빠른 성능을 낼 수 있을것으로 기대했지만, JVM 자체의 성능 이슈와 다른 여러가지 상황때문에 중간에 중단되었지만, 일부 회사들의 지원으로 JavaScript framework은 분리되어 Rhino가 되었다.

Rhino의 가장 큰 특징은 내부적으로 Reflection을 이용하여 JavaScript 코드에서 Java class를 그대로 가져다 쓸 수 있다는 것이다.
또한 Java구현체를 그대로 사용할 수 있기 때문에, JavaScript engine 중에서는 특이하게 multi thread support가 된다는 특징을 가진다.

JVM이 꾸준히 성장하여 많은 성능 개선을 이루었지만, WebKit이 사용하는 JSC(JavaScript Core)나 Google이 개발한 v8 engine도 내부적으로 JavaScript를 compile하기 때문에 Rhino가 가지는 성능상의 이점은 없다. 사실상 v8이나 jsc보다 느리다.

성능상에 이점은 없지만, 반드시 Java를 사용해야 하거나 multi-core support가 필요한 일부 환경에서는 Rhino engine을 사용하는 경우가 있다. 하지만 이 중에 이름만 들어서 알만한 유명한 프로젝트는 없다.
Rhino를 사용하는 가장 유명한 구현체는 RingoJS로 보인다.
http://blog.seulgi.kim/2014/06/rhino-javascript-framework.html

제가 생각하기에 흥미롭고 유횽한 기능들 위주로 살펴 보도록 하겠습니다.

3.1. Print Function

Nashorn은 arguments를 문자열로 변환 한 후 표준 출력(stdout)으로 출력하는 기능을 제공합니다.

1
2
print("Hello", "World");
// Hello World

echo()함수는 print() 함수와 동일한 기능을 합니다.
Nashorn에는 브라우저에서 주로 로깅에 사용하는 console object를 가지고 있지 않습니다.

3.2. Stdin에서 읽기

stdin에서 읽으려는 경우 readLine() 함수를 사용할 수 있습니다.

1
2
3
4
jjs> var name = readLine("What is your name? ")
What is your name? John
jjs> print("Hello, ${name}!")
Hello, John!

3.3. Files에서 읽기

stdin이 충분하지 않으면 readFully()를 사용하여 전체 파일 내용을 변수로 읽을 수 있습니다.

1
var content = readFully('text.txt');

3.4 문자열 Interpolation

${expression} 구문을 사용하여 문자열 리터럴에 표현식을 지정할 수 있습니다. 문자열 값은 ${expression} 표현식의 이름을 해당 변수의 값으로 대체하여 계산됩니다.

1
2
3
4
var name = "World";
var str = "Hello, ${name}!";
print(str);
// Hello, World!

3.5 Back-quote 실행 표현식

Nashorn은 back quote(역 따옴표) 문자열과 같은 유닉스 shell을 지원합니다. back quote로 묶인 문자열은 ‘exec’로 실행되고 리턴된 문자열을 프로그램에서 받을 수 있습니다.

test.js

1
2
3
4
5
6
7
#!/usr/bin/jjs
var files = `ls -l`; // get file listing as a string
var lines = files.split("\n");
for (var l in lines) {
var line = lines[l];
print(line);
}

이방법은 ES6의 새로운 역 따옴표 문자열 템플릿과 함께 사용하지 마십시오. 위에서 언급 한 문자열 Interpolation과 유사합니다!

3.6. Java Beans

Nashorn을 사용하면 Java bean의 gettersetter로 작업하는 대신 Java bean에서 값을 가져 오거나 설정하는 데 간단히 속성 이름을 사용할 수 있습니다.

1
2
3
4
5
var Date = Java.type('java.util.Date');
var date = new Date();
date.year += 1900; // -> no setter!
print(date.year); // -> no getter!
// 2016

3.7. Function Literals

간단한 한 줄 함수의 경우, 반드시 중괄호를 사용할 필요는 없습니다 (또한 return 키워드도 생략할 수도 있습니다).

1
2
3
4
5
function sqr(x) x * x;
print(sqr(3)); // 9
function add(a, b) a + b;
print(add(1, 2)); // 3

3.8. Binding Properties

두 개의 서로 다른 객체의 속성을 함께 바인딩할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
#!/usr/bin/jjs
var o1 = {};
var o2 = { foo: 'bar' };
Object.bindProperties(o1, o2);
print(o1.foo); // bar
o1.foo = 'John';
print(o2.foo); // John

3.9. Trimming Strings

trim()뿐만 아니라 trimLeft(), trimRight()를 이용하여 문자열의 white space를 제거할 수 있습니다.

1
2
3
4
5
6
7
8
#!/usr/bin/jjs
var bar = " bar";
var foo = "foo ";
print ("[" + bar.trim() +"]"); // [bar]
print ("[" + bar.trimLeft() + "]"); // [bar]
print ("[" + foo.trimRight() + "bar" + "]"); // [foobar]

3.10. Whereis

만약 프로그램중에 현재의 위치를 알아야 할 경우 아래와 같이 사용할 수 있습니다.

1
print(__FILE__, __LINE__, __DIR__);

3.11. Import Scopes

때로는 한 번에 많은 자바 패키지를 import 해야할 수도 있습니다. 그럴 경우 JavaImporter 클래스를 with 문과 함께 사용하여 import한 Java 패키지의 모든 클래스 파일을 with 문의 로컬 scope 내에서 액세스할 수 있습니다.

1
2
3
4
5
var imports = new JavaImporter(java.io, java.lang);
with (imports) {
var file = new File(__FILE__);
System.out.println(file.getAbsolutePath()); // /path/to/my/script.js
}

3.12. Packages

Pacakges및 연관된 객체들이 script내에서 Java 패키지에 대한 액세스를 지원합니다. Packages 변수의 속성은 모두 Java, javax와 같은 최상위 Java 패키지입니다.

1
2
3
4
5
6
7
8
var Vector = Packages.java.util.Vector;
// but short-cuts defined for important package prefixes like
// Packages.java, Packages.javax, Packages.com
// Packages.edu, Packages.javafx, Packages.org
var JFrame = javax.swing.JFrame; // javax == Packages.javax
var List = java.util.List; // java == Packages.java

jjs에서 다음을 테스트 해보세요

1
2
3
4
5
6
7
8
9
10
jjs> Packages.java
[JavaPackage java]
jjs> java
[JavaPackage java]
jjs> java.util.Vector
[JavaClass java.util.Vector]
jjs> javax
[JavaPackage javax]
jjs> javax.swing.JFrame
[JavaClass javax.swing.JFrame]

3.13. Typed Arrays

Native JavaScript의 배열은 타입이 지정되지 않습니다. 하지만 Nashorn은 ECMAScript 2015 명세에 지정된 유형화된 배열을 구현합니다.

예를 들어 아래와 같이 int형 타입을 가진 배열을 만들수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var IntArray = Java.type('int[]');
var array = new IntArray(3);
array[0] = 3;
array[1] = 2;
array[2] = 1;
try {
array[3] = 0; // would be possible in pure JS
} catch (e) {
print(e.message); // Array index out of range: 3
}
array[0] = '42';
print(array[0]); // 42
array[0] = 'some wrong type';
print(array[0]); // 0
array[0] = '3.14';
print(array[0]); // 3

int[] 배열은, 실제의 Java int 배열과 같이 동작합니다. 또한 Nashorn은 정수가 아닌 값을 추가하려고 할 때 implicit 한 형변환을 수행합니다. 또한 가능한 경우 문자열은 자동으로 int로 변환됩니다.

3.14. Collections and For-Each

자바 스크립트에서의 배열 처리는 때때로 짜증날때가 있습니다. 이제 Javascript의 배열의 사용 대신 모든 자바 컬렉션을 사용할 수 있습니다. 먼저 Java.type을 통해 Java 유형을 정의한 다음 필요에 따라 새 인스턴스를 작성하십시오.

1
2
3
4
5
6
7
var ArrayList = Java.type('java.util.ArrayList');
var list = new ArrayList();
list.add('a');
list.add('b');
list.add('c');
for each (var el in list) print(el); // a, b, c

컬렉션과 배열을 순회하는 Java의 foreach문처럼 Nashorn에는 for each문이 있습니다.
다음은 HashMap을 사용하는 다른 컬렉션의 for each 예제입니다.

1
2
3
4
5
6
7
var map = new java.util.HashMap();
map.put('foo', 'red');
map.put('bar', 'green');
for each (var e in map.keySet()) print(e); // foo, bar
for each (var e in map.values()) print(e); // red, green

3.15. Array 변환

java.util, java.lang과 같은 일부 패키지는 Java.type 또는 JavaImporter를 사용하지 않고 직접 액세스할 수 있습니다.

1
2
3
4
var list = new java.util.ArrayList();
list.add("s1");
list.add("s2");
list.add("s3");

아래 코드는 Java List를 Native JavaScript Array로 변환합니다.

1
2
3
var jsArray = Java.from(list);
print(jsArray); // s1,s2,s3
print(Object.prototype.toString.call(jsArray)); // [object Array]

그리고 그 반대의 경우는 아래와 같은 코드를 사용할 수 있습니다.

1
var javaArray = Java.to([3, 5, 7, 11], "int[]");

3.16. Lambdas and Streams

Java 8의 Lambda와 Streams을 Nashorn에서도 사용할 수 있습니다. 그리고
ECMAScript 5.1에는 Java 8 Lambda의 간단한 화살표 구문( -> )이 없습니다. 하지만 Java 8 Lambda가 허용되는 곳에서는 함수 리터럴을 사용할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var list2 = new java.util.ArrayList();
list2.add("ddd2");
list2.add("aaa2");
list2.add("bbb1");
list2.add("aaa1");
list2.add("bbb3");
list2.add("ccc");
list2.add("bbb2");
list2.add("ddd1");
list2
.stream()
.filter(function(el) {
return el.startsWith("aaa");
})
.sorted()
.forEach(function(el) {
print(el);
});
// aaa1, aaa2

Java 8 Lambda 또는 SAM (single-abstract-method) 유형이 필요한 곳에서는 ECMAScript 함수를 인수로 전달할 수 있습니다!

3.16.1. 모든 람다식은 JavaScript Function 입니다.

람다 형식의 인스턴스인 모든 Java 객체는 JavaScript Function처럼 취급 될 수 있습니다.

1
2
3
4
5
6
7
8
9
10
var JFunction = Java.type('java.util.function.Function')
var obj = new JFunction() {
apply: function(x) { print(x * x) }
}
print(typeof obj); // prints "function"
// 'calls' lambda as though it is a function
obj(23);

3.17. Extending Classes

Java 타입은 Java.extend 함수를 사용하여 간단히 확장할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var Runnable = Java.type('java.lang.Runnable');
var Printer = Java.extend(Runnable, {
run: function() {
print('printed from a separate thread');
}
});
var Thread = Java.type('java.lang.Thread');
new Thread(new Printer()).start();
new Thread(function() {
print('printed from another thread');
}).start();
// printed from a separate thread
// printed from another thread

위 코드에서 보듯이 Nashorn에서는 멀티 스레드 코드도 가능합니다.

3.18. Calling Super

ECMAScript에 Java의 super에 해당하는 키워드가 없기 때문에 JavaScript에서는 오버라이드된 멤버를 액세스하는 것은 어렵습니다. 하지만 Nashorn에서는 가능 합니다.

먼저 Java 코드에서 super class를 정의합니다.

1
2
3
4
5
6
class SuperRunner implements Runnable {
@Override
public void run() {
System.out.println("super run");
}
}

다음으로 JavaScript로부터 SuperRunner를 오버라이드(override) 합니다.
새로운 Runner 인스턴스를 생성할때 확장된 Nashorn 구문을 사용하는데 주의하세요.
오버라이드 멤버 구문은 Java의 익명 객체를 이용하여 정의 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
var SuperRunner = Java.type('my.package.SuperRunner');
var Runner = Java.extend(SuperRunner);
var runner = new Runner() {
run: function() {
Java.super(runner).run();
print('local run');
}
}
runner.run();
// super run
// local run

Java.super 확장을 활용하여 재정의된 메소드 SuperRunner.run ()을 호출합니다.

3.19. Loading Scripts

JavaScript에서 추가 스크립트 파일을 실행 하는 것은 매우 쉽습니다. load 함수를 사용하여 로컬 또는 원격 스크립트를 로드할 수 있습니다.

아래의 예제는 moment.js와 Underscore.js를 로드해서 사용하는 예제입니다.

1
2
3
4
5
load('https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.11.1/moment.min.js');
var now = new moment();
print(now);
// Thu Dec 31 2015 23:59:59 GMT+0100
1
2
3
4
5
6
7
oad('http://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.6.0/underscore-min.js');
var odds = _.filter([1, 2, 3, 4, 5, 6], function (num) {
return num % 2 == 1;
});
print(odds); // 1, 3, 5

외부 스크립트는 동일한 JavaScript context에서 evaluate 되므로 moment, Underscore를 직접 액세스할 수 있습니다.

3.19.1. Load in new Global Context

외부 파일을 로드할 때 로드된 코드에 자신의 코드에 있는 동일한 변수 이름이 사용될 수 있습니다. 이를 방지하기 위해 파일을 새로운 global context으로 로드할 수 있습니다.

1
loadWithNewGlobal('script.js');

위와 같이 로드된 스크립트는 Nashorn glogal context (현재 engine context가 아님)에서만 사용할 수 있습니다.

3.20. Error Object

Nashorn은 ECMAScript의 표준 Error 객체에 몇 가지 흥미로운 속성을 추가 하여 확장했습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function func() {
throw new Error();
}
function f() {
func();
}
try {
f();
} catch (e) {
print(e.stack);
print(e.lineNumber);
print(e.columnNumber);
print(e.fileName);
// of course, also this is possible
e.printStackTrace();
}

3.21. Scripting Mode Extension Objects

Nashorn에는 -scripting 모드로 실행 되었을때 사용할 수 있는 몇몇 global 객체가 정의되어 있습니다.
대부분은 바로 알수 있는 부분이라 아래의 소스 코드를 참고하길 바랍니다.

3.21.1. $ARG

1
2
3
4
5
$ jjs -scripting -- arg1 arg2 arg3
jjs> $ARG
arg1,arg2,arg3
jjs> $ARG[1]
arg2

arguments

1
2
3
4
5
6
$ jjs -scripting -- arg1 arg2 arg3
jjs> arguments
arg1,arg2,arg3
jjs> arguments[1]
arg2
`

3.21.2. $ENV

1
2
3
4
5
// print $JAVA_HOME and $PATH from the OS shell
print($ENV["JAVA_HOME"])
print($ENV["PATH"])
print($ENV.JAVA_HOME)
print($ENV.PATH)

3.21.3. $EXEC

Shell 명령어 수행을 위한 프로세스 실행

1
2
3
4
5
6
7
jjs> $EXEC("ls -l")
total 0
drwxr-xr-x+ 1 johndoe staff 4096 Dec 31 12:34 dir
-rwxrw-r-- 1 johndoe staff 168 Dec 31 13:37 file.txt
jjs> $EXEC("cat", "Send this to stdout")
Send this to stdout

3.21.4. $OUT

$OUT 변수는 가장 최근에 $EXEC로 실행된 프로세스의 stdout 출력이 저장됩니다.

1
2
3
4
5
6
// use curl to download JSON weather data from the net
var str = `curl http://api.openweathermap.org/data/2.5/weather?q=Hamburg,de&units=metric&appid=44db6a862fba0b067b1930da0d769e98`;
// parse JSON and print the current temperature
var weather = JSON.parse($OUT);
print(weather.main.temp);

3.21.5. $ERR

$ERR 변수는 가장 최근에 $EXEC로 실행된 프로세스의 stderr 출력이 저장됩니다.

3.21.6. $EXIT

$EXIT 변수는 $EXEC로 실행된 프로세스의 종료 코드가 저장됩니다.

3.21.7. $OPTIONS

이 속성은 Nashorn CLI의 실행 옵션을 제공합니다.

1
2
3
print("-scripting = " + $OPTIONS._scripting); // -scripting = true
print("--compile-only = " + $OPTIONS._compile_only); // --compile-only = false
print("-timezone = " + $OPTIONS._timezone.ID); // -timezone = Europe/Berlin

Nashorn이 구현한 다양한 언어 및 API 확장에 대한 설명은 Wiki 페이지에서 확인할 수 있습니다.

4. Working with Package Managers & Repositories

Nashorn으로 작업할 때, 처음부터 모든 기능을 개발하고 싶지는 않을 것입니다. 다행이도 Maven의 의존성 라이브러리를 사용하는 것처럼 이미 필요한 기능을 제공하는 Nashorn 라이브러리를 사용할 수 있습니다. 게다가 Java, JavaScript와 완벽히 호환되는 다양한 Repository와 Package Manager를 선택할 수도 있습니다.

4.1. NPM

Node Package Manager - https://npmjs.org

npm은 Node.js의 Package Manager이고 개발자들이 JavaScript 애플리케이션에서 파일, 메타데이터, 의존성을 쉽게 관리하도록 만들어졌습니다. 2009년 오픈 소스 프로젝트로 시작된 npm은 개발자들이 인터넷을 통한 서비스로 오픈 소스 코드를 관리할 수 있도록 패키지 저장소 기능을 제공합니다. (https://nodejs.github.io/nodejs-ko/articles/2015/06/17/npm-is-massive/)

  • NPM은 JavaScript의 server-side 프로그램인 Node.js 패키지 관리 프로그램입니다.
  • NPM Repository는 오픈 소스코드의 공개 컬랙션입니다.
  • 또한 NPM은 Command line 클라이언트 입니다.

Nashorn 내에서 NPM 패키지를 사용할 수 있습니다!. 하지만 …

4.1.1. JVM-NPM

대부분의 많은 JavaScript 패키지는 CommonJSrequire()문 (Java에서 import와 같은)을 사용합니다. 하지만 불행이도 Nashorn은 JavaScript 엔진일 뿐이고 Package Manager 및 dependency loading 매커니즘을 지원하지 않습니다.

하지만 다행이도 npm-jvm(JVM 용 NPM 호환 CommonJS 모듈 로더) 프로젝트가 있습니다. 이 모듈은 Nashorn script context에서 require() 함수를 사용할 수 있도록 합니다. Nashorn script context에서 CommonJS에 의존성이 있는 패키지를 사용하려 할 때 require() 함수를 사용하여 다른 모듈을 로드할 수 있습니다.

Usage
Nashorn이 제공하는 global load() 함수를 사용하여 jvm-npm.js를 global execution context에 로드합니다. 그런 다음 require()를 사용하여 원하는 모듈을 로드할 수 있습니다.

1
2
nashorn> load('./jvm-npm.js');
nashorn> var x = require('some_module');

또는 비슷한 역할을 하는 commonjs-modules-javax-script 프로젝트도 있습니다.

4.1.2. Polyfill.js

NPM에 등록된 많은 라이브러리는 Node.js 또는 브라우저 공통 API를 사용합니다. Nashorn은 Node.js도 브라우저도 아니기 때문에 라이브러리에서 사용하는 Node.js 또는 브라우저의 공통 API를 정의 하여야 합니다. 이러한 부분을 보통 polyfill이라 부르고 사용할 수없는 환경에서 필요한 기능을 제공하기 위해 사용합니다.

nashorn-polyfill.js

1
2
3
4
5
6
7
8
9
var global = this; // ---(1)
var window = this; // ---(2)
var process = {env:{}}; // ---(3)
var console = {}; // ---(4)
console.debug = print;
console.log = print;
console.warn = print;
console.error = print;

(1) Node.js에 있는 global variable을 위해 제공 (global context)
(2) 브라우저에 있는 window variable을 위해 제공 (global context)
(3) 몇몇 라이브러리에서 사용하는 Node.js의 process.env variable을 위해 제공
(4) Nashorn에는 console이 없기 때문에 가장 많이 사용되는 콘솔 출력 함수들을 print 함수에 할당합니다.

4.1.3. Native API access

Nashorn은 Native API (C/C++)를 액세스할 수 없습니다. 그래서 Native API를 사용하는 NPM 패키지는 Nashorn에서 사용할 수 없습니다.

4.2. Maven

Java 프로젝트 내에서 Nashorn을 사용할 때 대부분의 경우 Maven을 저장소 및 패키지 관리자로 사용합니다. 몇가지 plugin과 helper를 이용하면 문제 없이 사용할 수 있습니다.

4.2.1. Maven and NPM, Grunt, Gulp, etc.

Node.js ecosystem, NPM 그리고 프론트 엔드 빌드 도구인 Grunt, Gulp, Webpack등과 같이 사용하고 싶다면 아래의 편리한 Maven plugin을 사용할 수 있습니다.

https://github.com/eirslett/frontend-maven-plugin/ (이 plugin에 대한 자세한 설명과 문서는 Github 페이지를 참고하세요.)

4.2.2. WebJars

가장 많이 사용되고 인기 있는 NPM 패키지는 WebJars에서 제공하고 있습니다. WebJars를 사용하면 NPM 패키지를 프로젝트에서 Maven dependency를 이용하여 사용할 수 있습니다.

아래는 Moment.js를 Maven에서 사용하는 예제입니다.

1
2
3
4
5
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>moment</artifactId>
<version>2.11.2</version>
</dependency>

만약 사용할수 있는 NPM 패키지가 Webjar에 없다면 웹 페이지에 있는 자세히 설명된 문서를 이용하여 쉽게 추가할 수 있습니다.

4.2.3. Nasven

Nasven.js는 Maven을 이용하여 아티팩트의 dependency를 관리하는 JavaScript 애플리케이션(서버, 데스크탑 및 쉘 스크립트)의 런타임입니다.

개발자는 순수한 서버 측 응용 프로그램, 쉘 스크립트 또는 JavaFX 데스크탑 응용 프로그램을 만들 수 있으며 Maven dependency는 클래스 경로에 자동으로 다운로드되고 구성됩니다.

Nasven = Nashorn + Maven.

1
2
3
4
5
6
7
$ nasven package.json
nashorn full version 1.8.0_72-b15
[NASVEN] Building temporary Apache Maven project to find dependencies ...
[NASVEN] Done!
[NASVEN] About to run your nasven.js application under /Users/Niko/nasven/nasven-samples/camel/index.js ...
[NASVEN] Calling jjs for your application ...

예제

5. Isomorphic JavaScript

5.1 Isomorphic의 뜻, 그리고 무엇을 할 수 있고 왜 사용해야 하는가?

Isomorphic은 그리스어의 “isos”에서 “equal”의 의미를 , “morph”는 “shape”의 의미가 유래 되었습니다. 그래서, 동일하거나 동등한 형태의 뜻을 나타냅니다.

Isomorphism은 동일한 엔티티(1)에 대해 두개의 서로 다른 context(2) 에서 같은 결과(3)를 내야 한다고 설명합니다.(http://isomorphic.net/javascript)

  1. code
  2. client and server
  3. result, html, DOM, etc.

우리의 경우에는 “우리가 클라이언트와 서버에서 같은 JavaScript 코드를 사용한다면 우리는 같은 결과/html을 얻어야한다.”입니다.

이미지

5.1.1. Isomorphic code

Isomorphic 코드는 클라이언트와 서버 측에서 동일한 로직을 공유할 수 있으므로 DRY 원칙 (Don’t Repeat Yourself)을 활용한 코드 작성이 가능합니다. 애플리케이션 로직은 한번 개발되고 하나의 코드베이스에서만 유지 하면됩니다. 만약 오류가 발생하는 경우 코드의 수정은 한곳에서만 진행하면 됩니다. 그리고 Isomorphic 코드로 인해 개발자가 하나의 기술에만 집중할 수 있으므로 여러 프로그래밍 언어에 대한 전문가가 될 필요는 없습니다.

Example

아래의 예제는 브라우저에서 사용하기 위해 JavaScript로 개발 한 패스워드 검증 코드입니다.

1
2
3
4
function isPasswordValid(password) {
var score = scorePasswordStrength(password);
return score >= 3;
}

위 코드는 잘 작동하지만 만약 사용자가 브라우저에서 JavaScript를 사용하지 못하도록하고, 잘못된 암호를 서버에 전달하는 경우를 대비해, 서버(Java)에서 사용되는 프로그래밍 언어로 동일한 로직을 다시 개발해야합니다.
이것은 추가적인 노력이며, 구현할때 오류로 이어질 수 있고, 최근 변경된 로직에 대한 부분을 잊어 버리고 미구현할 수 있습니다.
Java 코드에서 JavaScript 코드 로직을 다시 사용하지 않을 이유가 있을까요? 아래 예제가 그 해법을 제시합니다.

1
2
3
4
5
6
7
public Boolean isPasswordValid(String password) {
try {
return (Boolean) nashorn.invokeFunction("isPasswordValid", password);
} catch (ScriptException | NoSuchMethodException e) {
throw new RuntimeException(e);
}
}

5.1.2. SPAs

현재 대부분의 SPA(Single Page Web Application)는 HTML 스켈레톤과 UI 렌더링, transition 관리, path routing, 네비게이션, 로직등을 하나 이상의 JavaScript 라이브러리에서 처리 합니다.

index.html

1
2
3
4
5
6
7
<html>
<head>
<title>Awesome Website</title>
<script src="./app-bundle.js"></script>
</head>
<body></body>
</html>

이러한 응용 프로그램은 대단히 훌륭하지만 몇 가지 단점이 있습니다.

단점

  1. UX/Performance - 전체 스크립트 코드가 클라이언트(브라우저)에서 로드, 평가 및 실행되기 전까지 사용자는 아무 것도 볼 수 없습니다.
  • 인터넷 연결 속도가 빠름에도 불구하고 Long wait time
  • 사용자가 떠날 가능성이 있음 (3초 룰)
  • 느린 인터넷 연결을 생각해 보십시오!
  1. Legacy Browser(아직 많이 사용하고 있습니다.)에서는 새로운 자바 스크립트를 실행시킬수 없습니다.
  2. SEO - 컨텐츠가 없기 때문에 검색엔진에서 여러분의 웹사이트를 인덱스할 수 없습니다.

    • Google이 웹 사이트에서 JavaScript를 실행(evaluate)할 수 있다고 해도 여전히 오류가 발생하기 쉽습니다.

위와 같은 이유로 웹 페이지를 서버에 렌더링하는 것이 더 좋을수 있습니다.

장점

하지만 이건 어떨까요?

  • UX - 멋진 화면 전환 효과 (다른 동종의 사이트들과 경쟁할 수 있어야 합니다.)
  • Performance - 페이지의 일부분만 빠르게 렌더링
  • Performance - 전체 HTML 대신 필요한 데이터만 전송

이러한 장점들은 클라이언트에서 프로그램을 실행하고 페이지를 렌더링할 때만 얻을 수 있습니다!

해결방법

  1. 사용자가 처음 URL을 요청합니다.
  2. 서버가 해당 URL의 콘텐츠를 가져옵니다.
  3. 서버에서 콘텐츠를 응답 형태로 렌더링합니다.
  4. 사용자는 콘텐츠를 봅니다.
  5. 그 동안에 클라이언트가 초기화됩니다.
  6. 사용자가 다른 URL로 이동합니다.
  7. 클라이언트가 해당 URL의 콘텐츠를 가져옵니다.
  8. 클라이언트가 DOM에 내용을 렌더링합니다.

사용자가 앱 또는 앱 경로에 대한 초기 요청할 때 페이지를 서버에서 렌더링하여 클라이언트에게 전송합니다.
그 순간부터 클라이언트는 콘텐트를 제어하고 콘텐트 렌더링을 계속할 수 있습니다.
이렇게하면 사용자가 초기 서버 측 렌더링 페이지를 사용하는 동안 클라이언트를 초기화하는 시간이 줄어듭니다.
그리고 검색 엔진에서 사이트의 인덱스를 생성하는 경우 이미 렌더링된 콘텐츠의 유효한 HTML을 서버에서 가져올수 있습니다.
그리고 기존 브라우저는 클라이언트가 페이지를 렌더링할 수 없는 경우에도 사이트를 사용할 수 있습니다.

5.2. React.js

React.js는 Facebook에서 개발 한 사용자 인터페이스를 구현한 JavaScript 라이브러리입니다. 앱을 빌드하기 위한 전체 스택 클라이언트 프레임 워크가 아닙니다. MVC 또는 MVVM에서 React.js는 V(iew) 부분을 컴포넌트로 만들기 위한 라이브러리입니다.

React.js는 컴포넌트 기반 가상 DOM을 가지고 있습니다. 이 가상 DOM과 비교후 다른 부분을 적용하여 깜박임없이 페이지 전환 또는 콘텐츠 업데이트가 가능합니다. 또한 React는 템플릿의 서버 측 렌더링을 지원합니다.

5.2.1. Flux

React를 이용하여 full-stack 애플리케이션을 구현하는 방법중에 Flux라는 아키텍처 방식이 있습니다.
이 아키텍처에는 직접 접근으로 변경할 수없는 Immutable Entitycollection이 있습니다. 응용 프로그램의 상태는 Store에 저장되고, 이 Store는 템플릿을 렌더링하기위한 데이터들이 저장됩니다. 또한 StoreActionEvent(데이터 포함)를 받은
Dispatcher만 수정할 수 있습니다. Action만이 Store가 외부 세계와 소통할 수있는 유일한 방법입니다.

현재 가장 널리 사용되는 Flux 구현 라이브러리는 Redux가 있고 다른 많은 라이브러리도 있습니다.

5.2.2. JSX

React.js는 JavaScript의 새로은 변형인 JSX를 HTML 요소와 혼합하여 많이 사용합니다. 아마도 처음에는 조금 어색할수 있지만 더 많이 사용하면 편리하다는 것을 느끼게 됩니다.

app.jsx

1
2
3
4
5
6
7
8
9
10
class Book extends React.Component {
render() {
return (
<div className="book">
<h3>{this.props.author}</h3>
<div className="lead">{this.props.children.toString()}</div>
</div>
);
}
}

(위의 코드를 볼때 몇몇분들은 JSP가 생각날 수도 있습니다….)

JSX는 Babel.js를 사용하여 실행 가능한 JavaScript (ES5)로 변환 됩니다. (이전에는 Facebook의 라이브러리 인 JSXTransformer에서이 작업을 수행했지만 더 강력한 Babel로 바뀌었습니다.)

app.js

1
2
3
4
5
6
7
8
9
10
var Book = React.createClass({displayName: "Book",
render: function () {
return (
React.createElement("div", {className: "book"},
React.createElement("h3", null, this.props.author),
React.createElement("div", {className: "lead"}, this.props.children.toString())
)
);
}
});

런타임시 위의 JavaScript 코드는 적절한 HTML로 렌더링됩니다.

app.html

1
2
3
4
<div class="book" data-reactid=".1c3dv7jhtco.1.$0">
<h3 data-reactid=".1c3dv7jhtco.1.$0.0">George Orwell</h3>
<div class="lead" data-reactid=".1c3dv7jhtco.1.$0.1">1984</div>
</div>

React는 데이터 변경시 data-reactid 속성을 통해 변경해야하는 (가상) DOM 부분을 찾을 수 있습니다.

React.js, JSX 및 Flux에 대한 자세한 내용은 해당 웹 사이트를 참조하십시오!

5.3. Isomorphic App을 위한 Spring Boot MVC

서버 측 렌더링을 위해 Spring (Boot) MVC 및 React.js를 사용하는 예제가 몇 가지 있습니다.

Spring MVC로 Isomorphic 응용 프로그램을 만드는 데 관심이 있다면이 위의 링크들을 참조하십시오.

5.4. Isomorphic App을 위한 Java EE 8 MVC 1.0

Java EE 8과 그 참조 구현인 Ozark에서 Action-based Web-Framework MVC 1.0을 새로 만들기 위해 React.js의 튜토리얼을 참고하여 ViewEngine 예제를 작성했습니다.

위의 두 저장소에서 가져온 아래 간단한 코드 조각은 JavaScript/Java EE 애플리케이션이 어떻게 Isomorphic으로 구현이 되는지 보여줍니다.

React 기반 ViewEngine을 사용하여 응용 프로그램을 만들때 위에서 언급한 저장소의 원래 코드를 사용해야합니다. 원래 코드들은 아래의 단순화된 예제보다 훨씬 더 유연하고 강력합니다!

ReactController는 새로운 @Controller 어노테이션을 사용한 표준 MVC 컨트롤러입니다.

ReactController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Controller // --- (1)
@Path("/react")
public class ReactController {
@Inject
private Models models; // --- (2)
@Inject
private BookService service; // --- (3)
@GET
public String index() throws Exception {
List<Book> books = service.getBooks();
models.put("data", books); // --- (4)
return "react:react.jsp"; // --- (5)
}
}

  1. MVC 컨트롤러 어노테이션
  2. Map 형식의 MVC 내부 Model Entity
  3. 데이터를 조회하거나 저장하는 서비스
  4. Book list를 가져와서 Java 오브젝트를 Model에 넣습니다.
  5. ReactViewEngine이 사용할 react: 접두어가 있는 템플릿의 Path를 반환합니다.

아래 예제가 React.js와 상호 작용하는 실제 ViewEngine 구현 클래스 입니다.

ReactViewEngine.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class ReactViewEngine extends ServletViewEngine {
private static final String viewPrefix = "react:";
@Inject
React react; // --- (1)
ObjectMapper mapper = new ObjectMapper();
@Override
public boolean supports(String view) { // --- (2)
return view.startsWith(viewPrefix);
}
@Override
public void processView(ViewEngineContext context) throws ViewEngineException { // --- (3)
// parse view and extract the actual template
String template = context.getView().substring(viewPrefix.length());
// get "data" from model
Models models = context.getModels();
Object data = models.get("data");
// call js function on data to generate html
String content = react.render(data);
// and put results as string in model
models.put("content", content);
try {
// additionally put the data as JSON also to the model
// this overrides the List data stored previously under the same key
models.put("data", mapper.writeValueAsString(data));
} catch (JsonProcessingException e) {
throw new ViewEngineException(e);
}
// create a new context with the actual view and forward to ServletViewEngine
ViewEngineContext ctx = new ViewEngineContextImpl(template, models,
context.getRequest(), context.getResponse(), context.getUriInfo(),
context.getResourceInfo(), context.getConfiguration());
try {
forwardRequest(ctx, "*.jsp", "*.jspx");
} catch (ServletException | IOException e) {
throw new ViewEngineException(e);
}
}
}

  1. React 클래스는 React.js JavaScript 코드와 상호 작용합니다. (자세한 내용은 아래 클래스를 참조하십시오.)
  2. supports() 메서드는 이 ReactViewEngine 클래스가 컨트롤러의 리턴 문자열에 적합한 ViewEngine으로 사용되어도 되는지 여부를 판단 합니다.
  3. processView() 메서드는 실제로 View를 처리하기 위한 일을 합니다. 자세한 내용은 인라인 주석을 참조하십시오.

아래 React 클래스는 JavaScript React.js와 상호 작용하는 클래스입니다.

React.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class React {
private ThreadLocal<ScriptEngine> engineHolder = ThreadLocal.withInitial(() -> { // --- (1)
ScriptEngine nashorn = new ScriptEngineManager().getEngineByName("nashorn");
try {
nashorn.eval(read("/nashorn-polyfill.js"));
nashorn.eval(read("/META-INF/resources/webjars/react/0.14.2/react.min.js"));
nashorn.eval(read(
"/META-INF/resources/webjars/showdown/0.3.1/compressed/showdown.js"));
nashorn.eval(read("/js/bookBox.js"));
} catch (ScriptException e) {
throw new RuntimeException(e);
}
return nashorn;
});
public String render(Object object) { // --- (2)
try {
Object html =
((Invocable) engineHolder.get()).invokeFunction("renderServer", object);
return String.valueOf(html);
} catch (Exception e) {
throw new IllegalStateException("failed to render react component", e);
}
}
private Reader read(String path) { // --- (3)
return new InputStreamReader(getClass().getClassLoader().getResourceAsStream(path));
}
}

  1. 필요한 JavaScript 라이브러리들과 함께 새로운 ThreadLocal<ScriptEngine>을 초기화 합니다. 왜냐하면 React.js는 Thread Safe하지 않기 때문에 각 요청에서 전용 ScriptEngine을 사용해야 합니다.
  2. JSX/JS 코드의 renderServer 함수를 호출하고 결과를 문자열로 반환합니다 (아래 코드 참조).
  3. 중복 코드 제거를 위한 메서드

아래 코드는 original JSX 코드입니다. (Nashorn에 로드하기 전에 JS로 렌더링 됨, Babel.js 라이브러리를 빌드 도중 또는 런타임에 Nashorn ScriptEngine에 로드할 수 있지만 로드 시간이 더 길어질 수 있음).

bookBox.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
... // --- (1)
var renderClient = function (books) { // --- (2)
var data = books || [];
React.render(
<BookBox data={data} url='books.json' pollInterval={5000} />, // --- (4)
document.getElementById("content")
);
};
var renderServer = function (books) { // --- (3)
var data = Java.from(books);
return React.renderToString(
<BookBox data={data} url='books.json' pollInterval={5000} /> // --- (4)
);
};
  1. 이전 코드 생략.
  2. 이 함수는 클라이언트에 의해 호출되며 응용 프로그램을 초기화하고 컨텐츠를 렌더링합니다.
  3. 이 함수는 서버 (위 React.java 참조)에 의해 호출되어 컨텐츠를 렌더링합니다.
  4. Component는 (1)에 있는 생략된 코드입니다. 실제 isomorphic 코드입니다.

추가 정보가 포함된 HTML skeleton이 서버에서 렌더링되고 클라이언트에 전송됩니다.

react.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ReactJS Bookstore with Ozark</title>
<script src="${mvc.contextPath}/webjars/react/0.14.2/react.min.js"></script> //--- (1)
<script src="${mvc.contextPath}/webjars/showdown/0.3.1/compressed/showdown.js"></script>
<script src="${mvc.contextPath}/webjars/jquery/1.11.3/jquery.min.js"></script>
<link href="${mvc.contextPath}/webjars/bootstrap/3.3.5/css/bootstrap.css" rel="stylesheet">
</head>
<body>
<div id="content" class="container">${content}</div> // --- (2)
<script type="text/javascript" src="${mvc.contextPath}/js/bookBox.js"></script> // --- (3)
<script type="text/javascript">
$(function () {
renderClient(${data}); // --- (4)
});
</script>
</body>
</html>

  1. WebJars를 이용한 JavaScript 라이브러리 (Maven/Gradle 빌드 파일에서 dependency 설정)
  2. Client 또는 Server에서 렌더링된 내용을 넣을 div
  3. 실제 응용 프로그램 스크립트 (bookBox.jsx 참고)
  4. 응용 프로그램이 클라이언트 측에서 실행 또는 초기화 될 때 호출 되는 함수입니다 (bookBox.js 참고)

ReactController에서 ReactViewEngine을 호출하지 않고 애플리케이션이 시작된 경우 (예를 들어 “react.jsp“만 반환하면 표준 JSP ViewEngine을 사용합니다.), 서버에서 수신 받은 초기 데이터를 보면 <div id = "content"/> 엘리먼트에는 눈으로 확인할 수 있는 HTML 코드가 포함되어 있지 않습니다. 하지만 renderClient()를 클라이언트에서 호출하기 때문에 브라우저에서 렌더링되고 컨텐츠가 표시됩니다.

프로그램에서 ReactViewEngine을 사용하면 서버에서 받은 초기 코드의 <div id = "content"/> 엘리먼트에는 렌더링된 컨텐츠가 표시 됩니다.

클라이언트의 renderClient() 함수도 실행이 되지만 (가상) DOM에는 변경 사항이 없으므로 페이지가 그대로 유지되고, DOM이 다시 렌더링되지 않아 깜박임 현상이 없어집니다.

6.JavaFX with JavaScript

http://www.n-k.de/riding-the-nashorn/#_javafx_with_javascript

JavaFx는 한국에서 별로 인기가 없어서 따로 번역을 하지 않았습니다.
자세한 설명은 원문을 참고하세요.

7. Nashorn Script 테스트와 디버깅 하기

7.1. Testing

Nashorn 스크립트의 테스트는 아직 사용해 보지 않아서 어려워 보일수 있지만 그렇지 않습니다.

7.1.1. Function(al) testing with JUnit

모든 JavaScript 함수에는 반환 값이 있으므로 (모든 JavaScript 함수, 명시 적 반환 값이없는 함수조차도 undefined를 반환합니다) 이러한 값을 지정할 수 있습니다.
Java 메서드를 개발하는 것처럼 JavaScript 파일을 빌드, 테스트 가능한 atomic 함수로 작업하면됩니다.
그런 다음 테스트에서 JavaScript 함수를 호출하고 결과를 assert 선언할 수 있습니다.

다음 JavaScript 코드가 있다고 가정 해 보겠습니다.

calculator.js

1
2
3
4
5
6
7
var add = function(a, b) {
return a + b;
}
var square = function(a) {
return a * a;
}

이제 다음 표준 JUnit 테스트를 사용하여 함수를 테스트할 수 있습니다

CalculatorTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class CalculatorTest {
ScriptEngine nashorn;
@Before
public void setup() {
nashorn = new ScriptEngineManager().getEngineByName("nashorn");
nashorn.eval(new InputStreamReader(
getClass().getClassLoader().getResourceAsStream("/calculator.js")));
}
@Test
public void testAdd() throws Exception {
Object result = ((Invocable) nashorn).invokeFunction("add", 1, 2);
Assert.assertEquals(1, result);
}
@Test
public void testSquare() throws Exception {
Object result = ((Invocable) nashorn).invokeFunction("square", 2);
Assert.assertEquals(4, result);
}
}

7.1.2. Mocking JavaScript functions & using Spock

Nashorn 스크립트를 테스트하는 훨씬 더 좋은 (더 강력한) 방법은 JUnit 대신 Spock을 사용하는 것입니다. Spock은 동적언어 Groovy 코드를 기반으로 작성되었습니다. 그렇기 때문에 Groovy로 만든 객체와 함수를 Nashorn과 테스트 하기 쉽습니다

Spock을 사용하면 직접 제어할 수 없거나 Nashorn에서 사용할 수없는 기능에 대해 mock function을 사용하기가 매우 쉽습니다. 이런 제어할수 없는 기능들 중에는 테스트 중에는 실행하고 싶지 않은 콜백 함수 (JavaScript에서 널리 사용됨), alert() 함수 등이 있을수 있습니다.

Callback.groovy

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class CallbackSpec {
@Shared ScriptEngine nashorn = new ScriptEngineManager().getEngineByName('nashorn');
def "callback"() { // -- (1)
given:
nashorn.eval('function callMe(callback) { return callback("") + " Doe"; }')
when:
def result = nashorn.invokeFunction('callMe', { return 'John' } as Function)
then:
result == 'John Doe'
}
def "mocked callback"() { // -- (2)
given:
nashorn.eval('function callMe(callback) { return callback("") + " Doe"; }')
def callback = Mock(Function)
when:
def result = nashorn.invokeFunction('callMe', callback)
then:
1 * callback.apply('') >> 'John'
result == 'John Doe'
}
def "alert"() { // -- (3)
given:
nashorn.eval('function alertMe() { alert("Huh!"); }')
def alert = Mock(Function)
nashorn.put('alert', alert)
when:
nashorn.invokeFunction('alertMe')
then:
1 * alert.apply('Huh!')
}
}

  1. 이 함수는 테스트 호출시 inject된 다른 콜백 함수 (java.util.function.Function 유형)를 가져옵니다.
  2. 이 함수에서 콜백 함수는 mock 함수로 대체 되고, mock 함수가 “John”값으로 한 번 호출된 이후에 테스트됩니다.
  3. Nashorn에는 사용할수 있는 alert() 함수가 없기 때문에 실제 함수가 호출되기 전에 Nashorn context에 추가해야합니다.

Spock과 Nashorn의 더 많은 예제는 GitHub 저장소 dasniko/nashorn-spock-jasmine에서 찾을 수 있습니다.

7.2. Debugging

Nashorn JavaScript의 디버깅은 다음과 같은 주요 IDE에서 지원됩니다.


참고

공유하기