java에서 python 코드를 사용하려면 jep 을 이용해서 기존코드 (파이선모듈의 C 종속성 상관없이)를 그대로 활용할수 있다. 또한 python 에서 java 코드를 사용하는것도 가능하다

기존에 python pandas로 작성된 코드를 java 환경에서 사용해야 하는 경우가 있다. pandas 는 기여자가 3000 명이 넘는, 수많은 기능을 포함하고 있는 방대한 라이브러리다. 이런 라이브러리를 이용해서 개발된 python 코드를 어떻게 java 에서 어떻게 사용할 것인가? 그 방법을 찾다보니 알게된 tablesawjep 이라는 오픈소스에 대해 간단히 적어본다.

tablesaw

pandas 의 dataframe 과 비슷한 Table 이라는 클래스를 제공한다. 단어 그대로 데이터들로 구성된 단순한 테이블이다. 이것은 pandas를 대신 할 수 있는게 아니고 직접 구현할 때 필요한 일부 기능이라고 할수 있다. pandas 의 index 라는 개념은 없고, 그냥 column, row 만 존재한다.

장점

  • dataframe 과 흡사한 데이터구조가 필요한 경우 사용하기 편함
  • 특이하게도 Matplotlib과 흡사한 visualization 기능도 제공

단점

  • pandas 만큼 다양한 함수가 없음
  • import, export, 필터링 등 기능을 제공하지만 pandas 에 비해 많이 부족한 수준
  • 그래서 필요한데 제공 안되는 함수는 직접 로직을 구현해야 한다. (헉?)
  • vectorization 미지원 (필요시 for loop 로 직접 구현)
  • 신규 개발이 거의 중단 상태로 보인다.

예를들어 pivot 의 경우, 아주 간단하게 제공을 해주지만 pandas 에서 사용하던 방식대로 사용하려면 직접 그 로직을 구현해야 한다. merge 도 마찬가지로 pandas 처럼 다양한 함수인자는 없고, 필요한 기능은 직접 구현 해야 한다. 최근 프로젝트에서 알아볼 시간도 없어서 그냥 이것으로 개발했는데, pandas 함수를 구현하고 결과가 python 과 동일한지 검증 하는 방식으로 개발을 해야 했다. 비록 원하는 결과를 얻긴 했지만 이런 식의 개발은 전혀 바람직하지 않다는 시행 착오를 겪게 되었다.

jep

java 에 임베디드된 python (jep 인터프리터)으로, JNI 와 CPython API 를 이용해서 JVM 내부에서 Python 인터프리터를 시작해서 python 호출을 가능하게 해준다.

python 인터프리터(정확하게 말하자면 jep 인터프리터)가 python 코드를 수행하는 것이니, 이미 작성되어 있는 python 코드를 아무런 수정없이 실행 가능하다 (이것이 pandas, numpy 등 C 에 종속된 특정 python 모듈 사용도 가능한 큰 장점임).

그리고 jep 은 java, python 간에 데이터를 연동 할 수 있게 해준다.

호오

지금부터 jep 설치와 예제를 수행 해 보도록 한다.

다음처럼 설치 하면

pip install jep

python 모듈 저장소에 jar 파일이 저장된다 (windows기준).

C:\Python311\Lib\site-packages\jep\jep-4.2.0.jar

이것을 classpath 에 추가한다 (intellij 등의 IDE 를 사용하는 경우에는 라이브러리로 추가).

그리고 window 환경변수 path 에 다음 경로를 추가한다

C:\Python311\Lib\site-packages\jep

java 에서 jep 인터프리터를 사용해서 python 을 실행하는것은 다음과 같다.

package org.kojh;

import java.nio.file.Files;
import java.nio.file.Path;
import jep.Interpreter;
import jep.SharedInterpreter;

public class Main {

    public static void main(String[] args) throws Exception{
        try (Interpreter interp = new SharedInterpreter()) {
            interp.exec("import pandas as pd");
            interp.exec("import numpy as np");
            interp.exec("dates = pd.date_range('1/1/2000', periods=8)");
            interp.exec("df = pd.DataFrame(np.random.randn(8, 4), index=dates, columns=['A', 'B', 'C', 'D'])");
            Object result1 = interp.getValue("df");
            System.out.println(result1);
        }
    }
}

python 스크립트 파일도 실행 가능하다. 다음과 같이 test.py 라는 python 코드를 java 에서 호출하는 경우를 가정해본다.

import pandas as pd
import numpy as np

df1 = pd.DataFrame({'lkey': ['foo', 'bar', 'baz', 'foo'], 'value': [1, 2, 3, 5]})
df2 = pd.DataFrame({'rkey': ['foo', 'bar', 'baz', 'foo'], 'value': [5, 6, 7, 8]})
merged1 = df1.merge(df2, left_on='lkey', right_on='rkey')
merged2 = df1.merge(df2, left_on='lkey', right_on='rkey', suffixes=('_left', '_right'))

df = pd.DataFrame({'a': [10., 20., 30., 40., 50.],
                   'b': [None, None, None, None, 500]},
                  index=pd.DatetimeIndex(['2018-02-27 09:01:00',
                                          '2018-02-27 09:02:00',
                                          '2018-02-27 09:03:00',
                                          '2018-02-27 09:04:00',
                                          '2018-02-27 09:05:00']))
asof_result = df.asof(pd.DatetimeIndex(['2018-02-27 09:03:30', '2018-02-27 09:04:30']))

그럼 다음처럼 호출해서 python 코드내의 변수 df에 접근할수 있다.

try (Interpreter interp = new SharedInterpreter()) {
    interp.runScript("test.py");
    System.out.println( interp.getValue("merged1"));
    Object result = interp.getValue("merged1");
    System.out.println(result);
    System.out.println( interp.getValue("asof_result"));
}

java <–> python 데이터를 주고 받기

java에서 python 으로 데이터를 전달 하거나 python 에서 처리한 데이터를 java 에서 전달 받을수 있어야 jep 을 사용하는 의미가 있을것이다. jep을 사용하면 String, Array 등의 기본 java type은 바로 python에서 str, list 로 자동 변환되어 사용할수 있다.

try (Interpreter interp = new SharedInterpreter()) {
    interp.set("input1", "123");
    interp.set("input2", Arrays.asList(1,2,3,4,5));
    interp.exec("type1 = type(input1)");
    interp.exec("type2 = type(input2)");
    System.out.println( interp.getValue("type1"));  // <class 'str'>
    System.out.println( interp.getValue("type2"));  // <class 'java.util.Arrays$ArrayList'>
    System.out.println( interp.getValue("input1")); // "123"
    System.out.println( interp.getValue("input2")); // [1, 2, 3, 4, 5]
}

그런데 pandas 의 dataframe 은 자동으로 변환되지 않는다. jep 이 pandas 전용이 아니므로 당연한 것이지만. 그래서, 아래 예제에서는 json 을 사용해서 python과 java 간 데이터 연동을 하고 있다.

  • java → python : map 혹은 json 을 전달해서 dataframe 생성할수 있다.
  • python → java : dataframe 을 json 으로 바꿔서 전달 할수 있다.
try (Interpreter interp = new SharedInterpreter()) {
    interp.exec("import pandas as pd");
    interp.exec("import numpy as np");
    interp.exec("import io");

    // --------------------------------------------------------------
    // java 에서 데이터를 넘겨서 python 에서 pandas dataframe 생성하기
    // 방법 1. map 을 전달해서 dataframe 생성
    Map<String, List<Object> > df = new HashMap<>();
    df.put("col1", Arrays.asList("val1","val2"));
    df.put("col2", Arrays.asList("val3","val4"));
    df.put("col3", Arrays.asList("val5","val6"));
    interp.set("input_map", df);
    interp.exec("dic = {} ");
    interp.exec("for key in input_map : dic[key] = input_map[key]");
    interp.exec("df_from_map = pd.DataFrame( dic ) ");
    interp.exec("df_from_map = df_from_map.sort_index(axis=1) ");
    System.out.println("\ndf from map ==> \n" + interp.getValue("df_from_map"));

    // 방법 2. json 을 전달해서 dataframe 생성
    String jsonDf ="{  \"col1\": {\"0\": \"val1\", \"1\": \"val2\"}, \"col2\": " +
        "{\"0\": \"val3\", \"1\": \"val4\"}, \"col3\": {\"0\": \"val5\", \"1\": \"val6\"}}";

    interp.set("input_json", jsonDf);
    interp.exec("df_from_json =  pd.read_json( io.StringIO(input_json) )");

    interp.exec("type = type(input_json)"); // <class 'str'>
    interp.exec("df_from_json = df_from_json.sort_index(axis=1) ");
    System.out.println("\ndf from json ==> \n" + interp.getValue("df_from_json"));

    // --------------------------------------------------------------
    // python 에서 json 으로 결과를 받고, 변경해서 다시 python 에 전달
    interp.exec("json_result = df_from_json.to_json() ");
    String json_result_from_python = interp.getValue("json_result", String.class);
    System.out.println("\njson result from python ==> \n" + json_result_from_python);

    JSONParser parser = new JSONParser();
    JSONObject jsonObject = (JSONObject) parser.parse(json_result_from_python);
    JSONObject col1JsonObj= (JSONObject) jsonObject.get("col1");

    // 받은 값을 변경해본다
    col1JsonObj.put("0", "XXX");
    col1JsonObj.put("1", "YYY");

    // 다시 python으로 전달..
    interp.set("input_json", jsonObject.toJSONString());
    interp.exec("df_from_json =  pd.read_json( io.StringIO(input_json) )");

    // 변경한 값이 제대로 전달 됐는지 확인
    interp.exec("type = type(input_json)"); // <class 'str'>
    interp.exec("df_from_json = df_from_json.sort_index(axis=1) ");
    System.out.println("\ndf from json ==> \n" + interp.getValue("df_from_json"));
}

jep을 사용해서 java에서 numpy 를 직접 사용하기 (소스 빌드 필요)

위 사용법처럼 java 에서 python 코드를 실행할때 numpy 사용 부분도 문제 없이 실행되는 것을 알 수 있었다. python 인터프리터(jep 인터프리터)가 임베디드 되어 돌아가는 것이니, 당연히 python 코드 내에서 그 어떤 모듈도 문제없이 사용할 수 있다.

그런데 java 에서 직접 numpy 함수를 호출하면서 (python 인터프리터 내부에서 수행되는게 아닌) 인자를 전달하고 그 결과도 받고 이런 작업을 하려면 jep 소스를 직접 컴파일 해서 jar 및 운영체계에 맞는 dll, 공유라이브러리 등을 재빌드 해야 한다. C 소스를 컴파일해야 하므로 운영체계에 맞는 컴파일러를 설치해야 한다. windows 경우에는 Microsoft Visual Studio 를 설치해야 하고, 현재 Community 2022 (64-bit) - 버전 17.11.4 이 설치된 상태다.

컴파일 하기 전에 먼저 다음 코드로 jep에서 numpy 직접 사용이 가능한 지 여부부터 확인해 본다.

interp.exec("result = jep.JEP_NUMPY_ENABLED");
System.out.println( interp.getValue("result")); // 0 or 1

1 이 출력되면 사용가능한것이고 0 이 출력되면 재빌드가 필요하다. 지금 상태에서는 0이 출력될 것이다.

그럼 일단 여기서 Numpy-Usage 공식 예제 하나를 수행 해 보면

try(SharedInterpreter interp = new SharedInterpreter()) {
   float[] f = new float[] { 1.0f, 2.1f, 3.3f, 4.5f, 5.6f, 6.7f };
   NDArray<float[]> nd = new NDArray<>(f, 3, 2);
   interp.set("x", nd);

   interp.exec("type = type(x)");
   System.out.println( interp.getValue("type"));
}

// <class 'jep.NDArray'>

제대로 동작하는 경우에는 인터프리터 내부에서 x라는 변수는 numpy.ndarray 타입의 shape는 (3,2)가 될것이고, <class 'numpy.ndarray'> 가 출력 되어야 한다. 그런데 <class 'jep.NDArray'> 가 출력되고 있으니 재빌드가 필요하다.

가장 먼저 할일은 numpy 부터 설치하는 것이다. jep 을 빌드할때 numpy 존재 여부를 먼저 확인하고 처리하기 때문이다. numpy 를 나중에 설치한다면 빌드를 다시 해야 한다.

pip install numpy

jep 소스를 받는다

git clone https://github.com/ninia/jep.git

그리고 컴파일 수행

python setup.py build

만약 다음과 같은 경고가 발생한다면

SetuptoolsDeprecationWarning: setup.py install is deprecated. Use build and pip and other standards-based tools.

setuptools 다운그레이드가 필요하다 (최신 버전으로는 성공하지 못했다) .

pip install setuptools==58.2.0

그리고 다시 컴파일을 수행한다

python setup.py build

다음처럼 numpy 설치여부를 확인해서, lib 를 다시 만드는 것을 볼수 있다.

jerem: ~/Downloads/mydev/java_dev/jep4.2.0# python setup.py build
numpy include found at C:\Python311\Lib\site-packages\numpy\core\include
running build
...
creating build\lib.win-amd64-3.11
creating build\lib.win-amd64-3.11\jep
copying src\main\python\jep\console.py -> build\lib.win-amd64-3.11\jep
...
building 'jep' extension
creating build\temp.win-amd64-3.11
...
4-3.11\jep\jep.cp311-win_amd64.pyd
   build\lib.win-amd64-3.11\jep\jep.cp311-win_amd64.lib 라이브러리 및 build\lib.win-amd64-3.11\jep\jep.cp311-win_amd64.exp 개체를 생성하고 있습니다.
코드를 생성하고 있습니다.
코드를 생성했습니다.
copying build\lib.win-amd64-3.11\jep\jep.cp311-win_amd64.pyd -> build\lib.win-amd64-3.11\jep\jep.cp311-win_amd64.dll
running build_scripts
creating build\scripts-3.11

새로 빌드한 파일로 기존 파일들을 대체한다 (windows기준 설명).

  • jep 소스폴더의 build\java\jep-4.2.0.jar 파일을 기존 C:\Python311\Lib\site-packages\jep\jep-4.2.0.jar로 복사한다.

  • build\lib.win-amd64-3.11\jep 에 생성된 모든 파일들을 C:\Python311\Lib\site-packages\jep 에 복사한다. 이때 jep.cp311-win_amd64.dll 은 jep.dll 로 rename 해서 복사해줘야 한다.

제대로 동작하는지 확인해 본다.

try (Interpreter interp = new SharedInterpreter()) {
    interp.exec("import pandas as pd");
    interp.exec("import numpy as np");
    interp.exec("result = jep.JEP_NUMPY_ENABLED");
    System.out.println( interp.getValue("result"));

    // For ease and speed of copying the arrays between the two languages,
    // the Java arrays must be one dimensional.
    float[] f = new float[] { 1.0f, 2.1f, 3.3f, 4.5f, 5.6f, 6.7f };

    NDArray<float[]> nd = new NDArray<>(f, 3, 2);
    interp.set("x", nd);
    interp.exec("type = type(x)");
    interp.exec("shape = x.shape");
    interp.exec("df = pd.DataFrame( np.array(x) ) ");
    System.out.println( interp.getValue("type"));
    System.out.println( interp.getValue("shape"));
    System.out.println( interp.getValue("df"));
}
// 1
// <class 'numpy.ndarray'>
// [3, 2]

python 에서 java 를 사용하기

jep 을 이용하면 python 내부에서 java 를 사용할수 있다.

try (Interpreter interp = new SharedInterpreter()) {
    //python 에서 java 함수 호출
    interp.exec("from java.lang import System");
    interp.exec("s = 'Hello World'");
    interp.exec("System.out.println(s)");
    interp.exec("print(s)");
    interp.exec("print(s[1:-1])");
    interp.exec("System.out.println(s[1:-1])");
}
// Hello World
// ello Worl

jep 은 python 과 흡사한 대화형 콘솔도 제공한다. pip install jep 로 설치하면 python이 설치된 Scripts 폴더(예: C:\Python311\Scripts)에 jep.bat 파일이 생성되는데, 이것을 실행하면 다음과 같은 대화창이 뜬다.

대화형console

또 jep 인터프리터를 사용해서 직접 python 파일(위 예제같이 java도 사용 할수 있는)을 실행 할수있다. python 파일을 하나 만들고 java 를 호출하게 작성 해 본다.

# py_java.py
from java.util import ArrayList, HashMap
//java 코드를 실행해본다
a = ArrayList()
a.add("abc")
a += ["def"]
print(a)

m = HashMap()
m.put("listkey", a)
m["otherkey"] = "xyz"
print(m)

다음처럼 python 파일을 실행해본다

jep py_java.py

java 코드가 잘 수행되는 것을 볼수 있다.

C:\Users\jerem\Downloads\mydev\java_dev\jep_test>jep py_java.py
C:\Users\jerem\Downloads\mydev\java_dev\jep_test>SETLOCAL
... 생략 ...
C:\Users\jerem\Downloads\mydev\java_dev\jep_test>SET jni_path="C:\Python311\Lib\site-packages\jep"
C:\Users\jerem\Downloads\mydev\java_dev\jep_test>SET args=py_java.py
C:\Users\jerem\Downloads\mydev\java_dev\jep_test>IF "py_java.py" == "" (SET args="C:\Python311\Lib\site-packages\jep\console.py" )
C:\Users\jerem\Downloads\mydev\java_dev\jep_test>java -classpath "C:\Python311\Lib\site-packages\jep\jep-4.2.0.jar" -Djava.library.path="C:\Python311\Lib\site-packages\jep" jep.Run py_java.py

[abc, def]
{listkey=[abc, def], otherkey=xyz}

C:\Users\jerem\Downloads\mydev\java_dev\jep_test>ENDLOCAL

jython, GraalPy

java와 python 을 연동하는것이 jep 만 있는건 아니다.

그런데 numpy (pandas) 등과 같이 C 언어에 종속성을 가진 python 모듈은 지원 안될 수 있다. 예를 들어 jython 이라는 것도 있지만 pandas, numpy 를 사용할수 없다.

오라클에서 만든 GraalPy (지랄파이)도 있는데, 이건 Python 뿐 아니라 JavaScript 등의 여러 언어를 내장하여 Java 애플리케이션을 확장할 수 있다고 한다.

그런데 테스트 해보니 일단 windows에서는 제약이 많아서 pandas 사용은 불가능했다. linux 인 경우에는 동작은 하는데, 약 30배 정도 속도 저하가 발생되었다. 그리고 장점으로 내세우고 있는 네이티브 바이너리로 컴파일 가능한 것도 시간이 너무 많이 걸렸다 (아주 간단한 것도 거의 10분).

이와 비교해보면 jep 은 성능이나 사용방식에 있어서 매우 좋은 선택이라고 생각된다.

참고

https://github.com/ninia/jep/wiki/Getting-Started https://github.com/ninia/jep/wiki/Accessing-Java-Objects-in-Python https://github.com/ninia/jep/wiki/Numpy-Usage https://github.com/ninia/jep/issues/560 https://github.com/ninia/jep/wiki/Windows