kotlin 으로 개발한 프로그램 내부에서 kotlin script를 실행하는 방법을 살펴보자.
발단
키-값 쌍의 문자열 리스트가 있다. 여기에 조작 규칙을 적용해서 추가적인 키-값 쌍을 만들고 싶다. 이런 조작 규칙은 가급적 컴파일 없이 바로 수정하고, 바로 실행할 수 있도록 하고 싶다.
예를 들어 [{"my_string" : "hello"}]
라는 입력이 있다면, 규칙을 적용해서
[
{"my_string" : "hello"},
{"my_string_v2" : "hello new version"}
]
이런 문자열 리스트를 만들고 싶다. 기존 프로그램은 gradle로 빌드하고, kotlin 으로 작성되어 있다.
처음엔 json으로 규칙을 만들어, 이걸 읽어들여 규칙 객체를 만드는 걸 생각했다. 여기선 $source$
가 원본 문구가 들어갈 placeholder라고 가정했다.
{ rules : [
{ "source_key" : "my_string",
"target_key" : "my_string_v2",
"convert" : "$source$ new version"
} ,
...
]
}
그런데 이렇게 구현하면, 저 원래 값의 placeholder를 나타내는 부분이 언제나 문제없이 동작하도록 규칙을 잘 선언하는게 쉽지 않다. 예를 들어 생성해야 하는 문구 자체가 $source$
라면 어떻게 하지? $source$ $source$
이렇게 선언할 수도 없고. 그러면 여기에 추가적으로 escaping 처리 등이 들어가야 해서 꽤 복잡해진다.
고민하다 잠시 뭔가 kts 같은 스크립트를 바로 실행할 수 없을까?
하는 생각이 들어 조금 뒤져보니 JSR 223 에 대응되는 kotlin 구현체가 있다.
자세한 내용은 라인의 엔지니어링 블로그 글, The power of Kotlin’s DSL and script engine 에 잘 나와있다.
하지만 저 블로그만 따라하면 java.lang.ClassNotFoundException: com.sun.jna.Native
이런 에러가 나면서 제대로 실행이 되지 않는다. 아마 저 글이 쓰여진 시점 이후로 뭔가 라이브러리 구조가 바뀐 것 같다. stackoverflow를 뒤져보니 역시 누군가 해결책을 제시했다.
JNA not found. native methods will be disabled. java.lang.ClassNotFoundException: com.sun.jna.Native
I am preparing spring-boot application. Then I got the following error:
JNA not found. native methods will be disabled. java.lang.ClassNotFoundException: com.sun.jna.Native
I am using swagger, elasticsearch, mariadb and maven for my project.
Log is given below:
11-02-2018 15:12:58 [o.e.env:120] internalInfo : [Araki] heap size [891mb], compressed ordinary object pointers [true]
…
저 답변에서 언급한 라이브러리를 쫙 넣으면 되는데, 과연 이게 다 필요한가 싶어 추려보니 몇개를 뺄 수 있었다.
본론
Kotlin 1.3.71 기준으로 gradle을 이용해 적용하는 방법은 다음과 같다.
1. build.gradle 에 의존 추가
dependencies {
implementation( "org.jetbrains.kotlin:kotlin-script-runtime:${Version.kotlin}")
implementation( "org.jetbrains.kotlin:kotlin-script-util:${Version.kotlin}")
implementation( "org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:${Version.kotlin}")
implementation( "org.jetbrains.kotlin:kotlin-scripting-jvm-host-embeddable:${Version.kotlin}")
implementation( "org.jetbrains.kotlin:kotlin-scripting-jvm-host:${Version.kotlin}")
implementation( "org.jetbrains.kotlin:kotlin-scripting-jsr223-embeddable:${Version.kotlin}")
}
2. javax.script.ScriptEngineFactory
파일 생성
META-INF/services
디렉터리 밑에 javax.script.ScriptEngineFactory
라는 파일을 만들고, 다음 한 줄을 내용으로 작성한다.
org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory
참고로 gradle 로 빌드한다면, /src/main/resources
디렉터리 밑에 META-INF
디렉터리를 만들면 된다.
3. 사용!
준비는 다 되었으니 이제 호출해서 사용하면 된다. 나의 경우엔 대략 다음과 같이 구현을 했다.
typealias ConvertRule = (String)->String
val ruleList = ScriptEngineManager(this::class.java.classLoader)
.getEngineByExtension("kts")
.eval(FileReader("my_script_file.kts")) as? List<Triple<String,String,ConvertRule>
이러면 처음 생각했던 json 보다 훨씬 직관적으로 규칙을 작성할 수 있다.
typealias ConvertRule = (String)->String
listOf<Triple<String,String,ConvertRule>(
Triple("my_string", "my_string_v2", { "${it} new version" })
)
저기에 확장 함수나 infix 연산자를 사용하면 좀 더 DSL스럽게 개선도 할 수 있다.
Top comments (0)