Kotlin 프로그램 내부에서 Kotlin script를 실행하기 - kotlin 1.3.71 기준
Tags: kotlin, kts, script
Published: 2020-05-27 08:16:49

---
title: Kotlin 프로그램 내부에서 Kotlin script를 실행하기 - kotlin 1.3.71 기준
published: true
description: JSR 223 스펙의 embedded kotlin script를 활용하기
tags: kotlin, kts, script
---

kotlin 으로 개발한 프로그램 내부에서 kotlin script를 실행하는 방법을 살펴보자.

# 발단

키-값 쌍의 문자열 리스트가 있다. 여기에 조작 규칙을 적용해서 추가적인 키-값 쌍을 만들고 싶다. 이런 조작 규칙은 가급적 컴파일 없이 바로 수정하고, 바로 실행할 수 있도록 하고 싶다.

예를 들어 `[{"my_string" : "hello"}]` 라는 입력이 있다면, 규칙을 적용해서 
```json
[
 {"my_string" : "hello"}, 
 {"my_string_v2" : "hello new version"}
]
```
이런 문자열 리스트를 만들고 싶다. 기존 프로그램은 gradle로 빌드하고, kotlin 으로 작성되어 있다.

처음엔 json으로 규칙을 만들어, 이걸 읽어들여 규칙 객체를 만드는 걸 생각했다. 여기선 `$source$` 가 원본 문구가 들어갈 placeholder라고 가정했다.

```json
  { rules : [
      { "source_key" : "my_string",
        "target_key" : "my_string_v2",
        "convert"    : "$source$ new version"
      } ,
      ...
    ]
  }
```

그런데 이렇게 구현하면, 저 원래 값의 placeholder를 나타내는 부분이 언제나 문제없이 동작하도록 규칙을 잘 선언하는게 쉽지 않다. 예를 들어 생성해야 하는 문구 자체가 `$source$` 라면 어떻게 하지? `$source$ $source$` 이렇게 선언할 수도 없고. 그러면 여기에 추가적으로 escaping 처리 등이 들어가야 해서 꽤 복잡해진다.

고민하다 잠시 `뭔가 kts 같은 스크립트를 바로 실행할 수 없을까?` 하는 생각이 들어 조금 뒤져보니 [JSR 223](https://jcp.org/en/jsr/detail?id=223) 에 대응되는 kotlin 구현체가 있다.

자세한 내용은 라인의 엔지니어링 블로그 글, [The power of Kotlin’s DSL and script engine](https://engineering.linecorp.com/ja/blog/kotlin-dsl-and-script-engine/) 에 잘 나와있다. 

하지만 저 블로그만 따라하면 `java.lang.ClassNotFoundException: com.sun.jna.Native` 이런 에러가 나면서 제대로 실행이 되지 않는다. 아마 저 글이 쓰여진 시점 이후로 뭔가 라이브러리 구조가 바뀐 것 같다. stackoverflow를 뒤져보니 역시 누군가 [해결책을 제시했다](https://stackoverflow.com/a/61338717/676215). 

{% stackoverflow 48730332 %}

저 답변에서 언급한 라이브러리를 쫙 넣으면 되는데, 과연 이게 다 필요한가 싶어 추려보니 몇개를 뺄 수 있었다.

# 본론

Kotlin 1.3.71 기준으로 gradle을 이용해 적용하는 방법은 다음과 같다.

### 1. build.gradle 에 의존 추가

```groovy
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. 사용!

준비는 다 되었으니 이제 호출해서 사용하면 된다. 나의 경우엔 대략 다음과 같이 구현을 했다.

```kotlin
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 보다 훨씬 직관적으로 규칙을 작성할 수 있다.

```kotlin
typealias ConvertRule = (String)->String

listOf<Triple<String,String,ConvertRule>(
        Triple("my_string", "my_string_v2", { "${it} new version" })
)
```

저기에 확장 함수나 infix 연산자를 사용하면 좀 더 DSL스럽게 개선도 할 수 있다.