>

ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Python] 자바 아카이브(JAR) 파일을 Python에서 호출하여 JSON 자료 주고받기
    Python 2024. 3. 21. 11:38

     

    최근에 프로젝트를 수행하던 중 Java로 작성된 소스코드를 Python에서 호출하는 방법에 대해 생각해보고 기능을 구현하였다. 흔한 케이스는 아니지만, 또 없는 케이스는 아닌 것 같아 정리했던 내용을 공유해보고자 한다.

     


    <개발환경>

    • Yocto Project 3.2.1
    • Uboot 2020.04
    • Linux Kernel 5.10.9
    • apt 1.8.2.1
    • bash 5.0
    • dpkg 1.2.0
    • Python 3.8.5
    • pip 20.0.2
    • OpenJDK 8

     

    개발환경은 나의 의지와 상관없이 Yocto Project 버전에 따라 하위 항목을 모두 맞추게 되어있다. 다른 것은 볼 필요 없고 Python 3.8.5와 OpenJDK 8만 보면 될 듯하다.

     

    Spring 프레임워크를 다룰 때 보통 OpenJDK 21를 사용하곤 했었는데, 8을 이용한 것은 이번이 처음이다. 근데 이것도 감지덕지다. apt를 통해서도 다운로드가 되지 않으며, arm 기반 32비트 jdk8u272-ga 버전을 설치해야해서 여러 곳을 찾아봤지만 tar.gz 파일도 없었다.

     

    결국, 빌딩 전에 레이어를 깔고 RECIPE에 추가하여 설치하는 방법밖에 없었고 이 부분은 전적으로 사수가 진행하였다. Yocto가 꽤 복잡하고 방대한 것으로 알고 있는데 매번 해결책을 찾는 것을 보면 대단하다.

     


    <Java>

    Java 소스는 타 업체로부터 전달받은 상태였고, 로직 수정이 일부 필요했다. 연산에 관한 로직은 그대로지만, 자료를 입력받는 방법과 변수를 설정하는 것, 그리고 최종적으로 산출된 값을 다시 Python으로 넘겨줘야했다.

     

    IDE는 IntelliJ Community 버전을 사용했고, SDK는 Oracle OpenJDK 21, 그리고 Language level을 8로 맞췄다. 이는 임베디드 리눅스에서 호출 시 Language level에 따른 클래스 미지원 등의 문제가 있었어서 동일하게 설정하였다.

     

    Project Structure

     

     

    Java의 아카이브 파일인 JAR 파일을 IntelliJ에서 만드는 방법은 굉장히 간단하다. 아래 그림처럼 Project Structure 내 'Artifacts'를 클릭하여 '+ 버튼'을 눌러 만들면 된다.

     

    현재 프로젝트에서 사용 중인 모듈을 자동으로 잡아주니 쉽게 설정할 수 있으며, 라이브러리들이 함께 들어가야하니 이 부분만 체크하면 된다. 이후 Build 메뉴에서 'Build Artifacts'를 눌러 빌드하면 JAR 파일이 생성된다.

     

    IntelliJ 내 JAR 파일 만들기

     

    빌드가 완료된 JAR 파일

     

     

    앞에 언급한 것 처럼 JAR 파일을 Python에서 호출하여 사용하기 위해 몇 가지 기존 로직에서 수정이 필요했는데, 그 내역은 다음과 같다.

     

    - 기존 로직 -

    1. 기존 Java 코드는 스케쥴러를 통해 4초마다 한 번씩 코드가 호출되는 구조였으며, 자료를 특정 경로에 생성되는 파일을 기준으로 변수를 입력 받아 연산이 이루어졌다.
    2. dr0라고 하는 변수를 하드코딩하여 매번 소스코드 수정이 이루어져야하는 구조였다.
    3. 연산이 완료되어 산출되는 4가지 파라미터에 대해 파일로 저장 및 DB에 INSERT되는 구조였다.

     

    - 수정된 로직 -

    1. 스케쥴러를 없애고, Python에서 호출이 있을 때 코드가 작동하도록 수정하였다.
    2. Python에서 배열을 담아 JSON 형태로 Java 내 전달되도록 하였다.
    3. gson 라이브러리를 활용하여 전달받은 자료에 대해 parsing 후 연산처리 로직이 정상 작동되도록 하였다.
    4. dr0 값을 하드코딩이 아닌 Python에서 함께 전달 받아 반영되도록 하였다.
    5. 파일로 저장 및 DB에 저장되는 구조가 아닌 다시 JSON 형태로 Python에 넘겨지도록 하였다.

     

    전체 코드를 다 첨부하기엔 너무 양이 많아 수정된 코드 일부만 첨부하였다. (계산 관련 코드는 모두 생략)

     

    private void processFile() {
        double[][] aVert = new double[2049][1];
        double[][] aNort = new double[2049][1];
        double[][] aEast = new double[2049][1];
        double dr0 = 0.0; // 초기화
    
        try {
            BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
            Gson gson = new Gson();
            String input = reader.readLine(); // JSON 데이터 읽기
    
            // 전체 데이터를 JsonObject로 파싱
            JsonObject jsonData = gson.fromJson(input, JsonObject.class);
    
            // dr0 값 추출
            if (jsonData.has("dr0")) {
                dr0 = jsonData.get("dr0").getAsDouble();
            }
    
            // XYZ 데이터 추출 및 변환
            if (jsonData.has("xyzData")) {
                JsonArray xyzData = jsonData.getAsJsonArray("xyzData");
                for (int i = 0; i < xyzData.size(); i++) {
                    JsonObject xyzObject = xyzData.get(i).getAsJsonObject();
                    aVert[i][0] = xyzObject.get("Z").getAsDouble();
                    aNort[i][0] = xyzObject.get("Y").getAsDouble();
                    aEast[i][0] = xyzObject.get("X").getAsDouble();
                }
            }
    
            // 스펙트럼 결과 계산 및 출력
            double[] spectrumResult = getSpectrumIndex(aEast, aNort, aVert, dr0);
            outputSpectrumResults(spectrumResult);
    
        } catch(Exception e) {
            e.printStackTrace();
        }
    }
    
    private double[] getSpectrumIndex(double[][] east, double[][] nort, double[][] vert, double dr0) {
        double[] retval = new double[4];
        try {
            retval = fnspectrum_parameter(east, nort, vert, dr0);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return retval;
    }
    
    private void outputSpectrumResults(double[] spectrumResult) {
        Gson gson = new Gson();
        SpectrumResults results = new SpectrumResults(spectrumResult[0], spectrumResult[1], spectrumResult[2], spectrumResult[3]);
        String jsonResults = gson.toJson(results);
    
        System.out.println(jsonResults); // 결과를 표준 출력으로 출력
        System.out.flush(); // 출력 버퍼 비우기
    }
    
    class SpectrumResults {
        double per;
        double fsprd;
        double tht;
        double dsprd;
    
        SpectrumResults(double per, double fsprd, double tht, double dsprd) {
            this.per = per;
            this.fsprd = fsprd;
            this.tht = tht;
            this.dsprd = dsprd;
        }
    }

     

     

    테스트 데이터는 약 2049개의 X,Y,Z 값이 배열 형태로 담긴 자료와 dr0를 Python 쪽에서 Java로 넘겨주는 형태다. 

     

    {"xyzData": [{"X": -0.203, "Y": -0.083, "Z": -0.025}, {"X": -0.051, "Y": 0.404, "Z": 0.091}, {"X": 0.204, "Y": 0.487, "Z": 0.049}], "dr0": 222.0}

     

    xyzData에 2049개 배열이 있는데 편의를 위해 3개만 나타냈다. dr0값도 함께 JSON 형태로 Java 소스코드에 전달되면 이를 processFile 메소드에서 읽어들여 연산 로직을 호출하는 구조이다.

     

    최종적으로, outputSpectrumResults 메소드 내 System.out.println(jsonResults)를 통해 출력된 결과를 Python 쪽에서 입력받아 저장할 수 있다.

     


    <Python>

    이 포스팅에선 Java를 메인으로 다룬 것이 아니기에 Python 코드에 대해 더 자세히 설명한다. 일단 외부 라이브러리 추가 필요 없이 'subprocess' 모듈을 이용해 커맨드를 날려 JAR 파일 실행이 가능하다. 소스코드는 다음과 같다.

     

    import subprocess
    import json
    
    class XYZ:
        X = 0
        Y = 0
        Z = 0
    
        def __init__(self, x, y, z):
            self.X = float(x)
            self.Y = float(y)
            self.Z = float(z)
    
    def createArray():
        arrHF_XYZ = []  
        filename = 'RIP_20230716004710.dat'
        #dr0 = 356.0
        dr0 = 222.0
    
        try:
            with open(filename, 'r', encoding='utf-8') as file:
                for line in file:
                    lineData = ' '.join(line.strip().split())
                    arr = lineData.split(' ')
                    if len(arr) > 8:  
                        arrHF_XYZ.append(XYZ(arr[6], arr[7], arr[8]))
            data_array = {
                'xyzData': [{'X': obj.X, 'Y': obj.Y, 'Z': obj.Z} for obj in arrHF_XYZ],
                'dr0': dr0
            }
            return json.dumps(data_array)
        except Exception as ex:
            print('createArray finish', ex)
            return None
    
    def callJava(data):
        jarPath = 'ripcurrent.jar'
        javaPath = '/usr/lib/jvm/openjdk-8/bin/java'
        #javaPath = 'C:\\Program Files\\OpenJDK\\JDK21\\bin\\java.exe'
    
        command = [javaPath, "-jar", jarPath]
    
        result = subprocess.run(command, input=data, text=True, capture_output=True)
        
        if result.stdout:
            try:
                #Java에서 받은 JSON 자료 출력
                spectrumResult = json.loads(result.stdout)
                #print("Received spectrum results:", spectrumResult)
    
                PER = spectrumResult.get('per')
                FSPRD = spectrumResult.get('fsprd')
                THT = spectrumResult.get('tht')
                DSPRD = spectrumResult.get('dsprd')
    
                print("PER:", PER)
                print("FSPRD:", FSPRD)
                print("THT:", THT)
                print("DSPRD:", DSPRD)
    
            except json.JSONDecodeError as e:
                print("Failed to decode JSON from Java output:", e)
        else:
            print("No received from Java")
    
    sendData = createArray()
    callJava(sendData)
    #print('Data send to Java: ', sendData)

     

     

    X,Y,Z를 담을 classXYZ를 하나 만들었다. 그리고 X,Y,Z 배열과 함께 dr0 값을 JSON으로 담는 createArray 함수를 작성했다.

     

    실제 운영환경에서는 센서가 2Hz 간격으로 입력되어 배열을 만들지만, 테스트 환경에서는 해당 입력자료를 임의의 dat 파일로 만들어 수행했다.

     

    RIP_20230716004710.dat 파일은 아래 그림처럼 '연, 월, 일, 시, 분, 초, X, Y, Z'로 구성되며 총 2049개의 line이 있다. 여기서 X, Y, Z값만 담아 전송할 데이터로 만든다.

     

    {"xyzData": [{"X": -0.203, "Y": -0.083, "Z": -0.025}, {"X": -0.051, "Y": 0.404, "Z": 0.091}, {"X": 0.204, "Y": 0.487, "Z": 0.049}], "dr0": 222.0} 처럼 말이다.

     

    테스트 데이터

     

     

    callJava 함수는 createArray에 의해 생성된 자료를 JAR를 호출하여 넘겨주고, JAR 로직에 의해 산출된 값을 다시 result로 받아 Python에서 확인할 수 있도록 하는 역할이다.

     

    subprocess 모듈의 run을 통해 JAR 파일 실행 및 입력 자료 넘김과 함께 출력 자료를 캡처할 수 있다. jarPath는 Python 소스코드와 동일한 위치에 있으며, javaPath는 각 임베디드 리눅스와 Windows 환경에서 테스트하기 위해 2개를 모두 작성했다.

     

    subprocess에 의해 sendData가 ripcurrent.jar에 전달되면, 그에 대한 출력값이 result.stdout으로 반환된다. 이때 result.stdout은 Java 코드에서 작성한 outputSpectrumResults 메소드에 의해 JSON 형태로 전달되기에 이를 parsing 후 사용하면된다.

     

    Windows 환경 실행 결과
    임베디드 리눅스 환경 실행 결과

     

     

    Windows 환경과 임베디드 리눅스 환경에서 모두 실행 결과 동일한 파라미터 값이 산출된 것을 확인할 수 있다. 처음에 임베디드 리눅스 환경에서 오류가 나서 jar 관련된 명령어를 직접 입력해보니 버전 오류가 나타났다.

     

    Windows 환경에서는 OpenJDK 21을 사용하고 있었고, 이를 빌드했기 때문에 문제가 발생한 것이다. 이에 위에 Java 파트에서 설명한 것 처럼 Language level을 8로 변경 후 재빌드한 결과 문제없이 실행되는 것을 확인했다.

     

    주로 웹개발과 관련된 업무를 수행하고, 또 사이드 프로젝트로 공부하다보니 Spring 프레임워크에만 관심이 있었는데 이번 계기로 Java 언어에 대한 장점을 배운 것 같다. 특히, 라이브러리를 포함한 전체 아카이브를 용량이 크지 않은 JAR 파일로 빌드하여 타 언어에서 사용할 수 있다는 점은 매우 훌륭하다.

     

    Java에 대해 조금 더 깊게 공부하는 계기가 될 것 같다.

     

     

    댓글

Designed by Tistory.