loading...

Kotlin 프로그램 내부에서 Kotlin script를 실행하기 - kotlin 1.3.71 기준

kingori profile image Sewon Ann ・1 min read

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를 뒤져보니 역시 누군가 해결책을 제시했다.

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스럽게 개선도 할 수 있다.

Posted on by:

kingori profile

Sewon Ann

@kingori

Android Developer in Korea

Discussion

markdown guide