JNI 적응기 (1) - 우리 회사는 왜 JNI를 사용할까?

게으른 개발자·2024년 11월 2일

현재 재직하고 있는 회사에서 내가 맡은 업무는 신분증 OCR, 사본 판별 솔루션 개발을 하고 있다. 지금은 물론 자주 들어서 JNI에 대해서는 대략적인 느낌으로 아는 것은 있지만, 처음 듣고서 기능의 개발을 해야 할 때는 진짜 막막했다... ㅜㅜ 지금도 개념은 모르는데 그냥 어찌저찌 구선생(?) 님을 통해서 열심히 해결하고 있지만 이번 기회에 정리해 보고자 한다.

서론

먼저 현재 재직하고 있는 회사의 주요 고객층은 금융업에 속해있는 회사들이 많다. 이 회사들의 가장 불편한 공통점은 엄청 레거시한 언어와 버전으로 프로젝트들을 관리한다... AI가 유행하는 지금 Python을 사용하는 고객사를 아직 보지를 못했다... Java도 20버전대가 나온 지가 언젠데 나는 아직 회사에서 8버전 위로 올라가 본 적이 없다... ㅠㅠ(개인 프로젝트만 최신 버전으로 사용해서 하고 있다!! 이렇게해도 나중에 이직은 가능하겠지...???)

그래서 대부분의 고객사에 납품할 수 있는 형식이 매우 제한되어 있는데 현 회사는 AI 모델을 onnx라는 것으로 변환하고 그걸 Java와 연결해주는 so 라이브러리 라는 것을 만들어 Java에서도 AI 모델을 사용할 수 있도록 해준다. 그때 그 연결을 도와주는 것이 JNI라는 것이다. 이제 한번 JNI를 자세하게 알아보도록 해보자. 대략 그림으로 표현하면 아래와 같다.(절대 정답은 아닙니다!!)

JNI 란?

JNI(Java Native Interface)는 Java 코드와 다른 프로개르밍 언어(주로 C/C++)로 작성된 네이티브 코드 간의 상호 작용을 가능하게 하는 인터페이스이다. Java가 JVM 위에서 실행되면서 네이티브 시스템 기능에 직접 접근하기 어려울 때, JNI를 통해 네이티브 코드를 호출하여 성능 최적화나 하드웨어 접근을 구현할 수 있다.

"출처 위키백과"

JNI의 기본 구조

  1. Java 선언
    • Java 메서드에 native 키워드를 사용해 네이티브 메서드를 선언
  2. JNI 라이브러리 로드
    • System.loadLibrary("{library_name}")을 통해 네이티브 라이브러리를 로드한다.
    • 라이브러리 형식
      • Windows: .dll (Dynamic Link Library)
      • Linux: .so (Shared Object)
      • macOS: .dylib (Dynamic Library)
  3. 네이티브 코드 작성
    • 네이티브 언어(C/C++)로 구현하고 Java가 호출할 수 있도록 JNI 함수 구조에 맞게 작성합니다.

현재까지 느낀 JNI의 장/단점

장점

  • 플랫폼 종속적 기능 접근
    • Java에서는 직접 제공하지 않는 플랫폼에 종속적인 기능(예: 운영체제의 특정 API, 하드웨어 인터페이스, 고유한 네이티브 라이브러리)에 접근 가능
  • 하드웨어 접근 가능
    • Java는 JVM에서 실행되어 하드웨어 접근이 제한적이지만, JNI를 사용하면 네이티브 하드웨어(GPU, 메모리 등) 리소스에 접근하여 제어할 수 있다.
  • 특수한 라이브러리 지원
    • OpenCV와 같은 이미지 처리 라이브러리나 CUDA와 같은 고성능 컴퓨팅 라이브러리와 Java와의 연결 지원

단점

  • 코드 이식성 저하
    • 네이티브 라이브러리는 플랫폼에 종속적이기 때문에 OS마다 별도로 컴파일이 필요하다.
      ex. 같은 Linux여도 Debian과 Redhat 계열간 네이티브 라이브러리도 별도로 컴파일이 필요하다. (RedHat 계열도 8.x 버전 간의 차이가 있어 환경을 새롭게 구축해서 컴파일 한적이 있다...ㅜㅜ)
  • 코드 복잡도 증가
    • Java와 네이티브 코드 간의 데이터 변환, 메모리 관리, 예외 처리 등으로 인해 코드가 복잡해질 수 있다.
  • 디버깅의 복잡성
    • Java 디버거로는 네이티브의 문제를 분석하기 어렵다
    • 문제의 원인 분석을 위해서 Java 어플리케이션의 설정 변경 등의 추가적인 리소스들이 들어간다.
  • 유지 보수의 어려움
    • Java 뿐만이 아니라 네이티브 단의 학습을 필요로 한다....(이게 제일 싫다.... 나는 ChatGPT가 없으면 못할 거 같다.)

변수 타입

  • C++에서 JNI를 통해 Java 객체 또는 메소드에 접근 할 때에 필요한 것들이 변수 타입과 그에 맞는 시그니처라는 것이 있다.

기본(primitive) 자료형

JavaC++/CC++/C 배열(참조 자료형)
booleanjbooleanjbooleanArray
bytejbytejbytArray
char jchar jcharArray
short jshort jshortArray
int jint jintArray
longjlong jlongArray
float jfloat jfloatArray
doublejdoublejdoubleArray

참조 자료형

JavaC++/C
Objectjobject
Classjclass
String string 
Array jarray 

기타 타입

C++/CDescription
jthrowable자바 예외를 나타내는 타입
jmethodID, jfieldID메서드나 필드의 식별자를 나타내는 타입

시그니처(Signature)

  • 예시
    • long l (int n, String s, int[] arr);
      (IL/java/lang/String;[I)J
signatureJava Type
Bbyte
Cchar
double 
float 
Iint
Jlong
short 
void 
Zboolean
L+ {클래스의 패키지 + 클래스 명} 해당 클래스
ex. String -> Ljava/lang/String;
[+{클래스 시그니처} 배열
ex. int[] -> [I 

예제(C++)

  • 우선은 개념만을 생각하며 구조만 파악하고 다음 장에서 좀 더 자세한 개념을 가지고 실무에서 주로 사용하는 방법을 적용해보자!! (예제만 하는데도 머리가 너무 아프다;;)

국민 예제 hello world 및 문자열 출력!

1. Java

public class JniTest {

    native void printHello();
    native void printString(String str);

    static {
        System.loadLibrary("jniTest");
    }
}
  1. JniTest.java 파일을 생성한 뒤, 터미널로 해당 파일의 경로로 이동
  2. javac {컴파일이 필요한 자바 파일명}
    • javac JniTest.java

2. JNI 헤더 파일 생성(JniTest.h)

  1. javah -cp {컴파일한 파일의 저장 경로(패키지 제외)} {패키지명.컴파일한 파일명}
    • javah -cp C:\Users\Public\jni_build\jni-test\java JniTest
    • 폴더명이 숫자로 시작하거나 하면 제대로 읽지 못한다.(되도록 영문 경로를 권장)
  2. 헤더 파일의 결과는 아래와 같다.
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class JniTest */

#ifndef _Included_JniTest
#define _Included_JniTest
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     JniTest
 * Method:    printHello
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_JniTest_printHello
  (JNIEnv *, jobject);

/*
 * Class:     JniTest
 * Method:    printString
 * Signature: (Ljava/lang/String;)V
 */
JNIEXPORT void JNICALL Java_JniTest_printString
  (JNIEnv *, jobject, jstring);

#ifdef __cplusplus
}
#endif
#endif

3. 네이티브 메소드 구현

#include "JniTest.h"

JNIEXPORT void JNICALL Java_JniTest_printHello(JNIEnv *env, jobject obj)
{
    printf("Hello World\n");
    return;
}

//C Version
#include <stdio.h>
JNIEXPORT void JNICALL Java_JniTest_printString(JNIEnv *env, jobject obj, jstring string)
{
    const char *str = (*env)->GetStringUTFChars(env, string, 0);
    printf("%s!\n", str);

    return; 
}

//C++ Version
#include <iostream>
JNIEXPORT void JNICALL Java_JniTest_printString(JNIEnv* env, jobject obj, jstring string) {
    const char* str = env->GetStringUTFChars(string, 0);
    std::cout << str << "!" << std::endl;
    env->ReleaseStringUTFChars(string, str);
}

4. C 공유 라이브러리 생성

  • Visual Studio에서 프로젝트 생성 후, [프로젝트 → 속성] “일반”, “VC++ 디렉터리” 페이지에서 아래 이미지와 같이 수정
  • 일반
  • VC++ 디렉터리
  • 앞에서 생성한 JniTest.h와 JniTest.c 를 생성한 Visual Studio 프로젝트로 이동
  • Visual Studio [빌드 → {프로젝트명}(JniTest) 빌드 or 솔루션 빌드]
  • 빌드된 파일 위치
    - {Visual Studio 프로젝트 경로}\x64\Debug

5. Java, Native Library 연동 테스트

  • Java Main 함수
public class JniMain {
    public static void main(String[] args) {
        JniTest jniTest = new JniTest();
        jniTest.printHello();
        jniTest.printString("jni test success;");
    }
}
  • 결과

감사합니다 GPT님!!

정리

물론 내가 배움의 속도가 느린 것도 있겠지만.... 이건 너무 레거시한거 아니냐고!!! JniTest.h 파일 빌드하는데 경로 상의 문제인지 모르고 이것저것 수정하다 보니까 2일을 허비해서.... 이번에 글을 업로드하는 시간이 너무 오래 걸렸다... 그래도 이번 기회에 JNI랑 조금 친해(?) 졌으니 다음 기회에는 진짜 내가 알고 싶었던 JNI를 왜 사용해야 하는지? 그리고 꼭 JNI만을 사용해야 하는지를 알아보자!!

반성문

핑계 아닌 핑계로 최근에 회사가 너무 바쁘기도 했고.... 여행도 가야 해서 공부할 시간이 없어 글을 작성할 시간이 없었는데 다녀온 뒤로는 이런저런 핑계 없이 힘내서 글을 열시미 써보자!!!

profile
6년차 백엔드 엔지니어로 일하고 있습니다~!

0개의 댓글