DEV Community

Sewon Ann
Sewon Ann

Posted on

4

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

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

Sentry image

Hands-on debugging session: instrument, monitor, and fix

Join Lazar for a hands-on session where you’ll build it, break it, debug it, and fix it. You’ll set up Sentry, track errors, use Session Replay and Tracing, and leverage some good ol’ AI to find and fix issues fast.

RSVP here →

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs