본문 바로가기
Old Posts/Java

[Java] JNI 사용법 및 예제

by A6K 2021. 4. 19.

자바는 가상 머신인 JVM위에서 실행되도록 만들어진 언어다. 프로그래머가 자바 언어로 작성한 프로그램은 JVM 위에서 동작하도록 중간 언어인 바이트 코드로 컴파일 된다. 운영체제나 아키텍처와 관련된 호환은 JVM이 신경써주기 때문에 하나의 소스코드로 작성한 프로그램을 플랫폼에 상관없이 실행할 수 있다. 다시말해서 자바로 작성된 프로그램을 윈도우에서도 실행할 수 있고, 리눅스에서도 동일하게 실행할 수 있다는 의미다.

JVM이 하는 이런 역할은 자바뿐 아니라 JVM 언어들의 강점이다. 하지만 운영체제가 제공하는 특정 기능들을 자바에서는 사용하기 힘들다는 단점도 있다. 리눅스 커널이 업데이트 되면서 새로운 시스템 콜이 추가되거나 특정 유닉스의 시스템 콜을 호출하고 싶지만 JVM이 지원하지 않으면 자바에서 사용하기는 매우 어려울 수 있다.

또 한, 구현하고 싶은 몇몇 기능들이 자바 언어로 작성될 경우 매우 비효율적일 수 있다. 이 경우 C언어 혹은 C++을 이용해서 컴파일한 모듈을 자바에서 호출해 사용하는 방법을 고려해봐야한다. 이런 방법 중 하나가 JNI(Java Native Interface)다.

JNI - Java Native Interface

JNI(Java Native Interface)는 자바에서 C 혹은 C++로 작성된 모듈을 호출할 수 있게 해주는 기능이다.

운영체제에 특화되어 있는 기능이나 고성능의 실행을 필요로하는 모듈을 C, C++ 같은 컴파일 언어로 구현한 다음 JVM을 통해 자바 프로그램에서 호출해 사용하는 방법이다.

JNI를 이용하는 자바 프로그램은 다음과 같다.

public class JNIExample {
	static {
		System.loadLibrary("MyJNI");
	}

	private native int getNumber();
	private native void printHelloWorld();

	public static void main(String[] args){
		JNIExample jni = new JNIExample();

		jni.printHelloWorld(); 

		System.out.println(jni.getNumber());
	}
}

 

클래스의 첫 부분을 보면 static 블럭이 있다. System.loadLibrary() 메소드를 이용해서 라이브러리를 동적으로 로딩한다.

그 다음 라인에서는 native 키워드를 이용해서 메소드를 선언해놓는다. 이렇게하면 System.loadLibrary()로 로드한 라이브러리에서 native 키워드로 선언된 getNumber(), printHelloWorld() 메소드를 찾아서 연결해준다. 네이티브 메소드는 껍데기일 뿐이다.

자바 파일을 빌드해준다.

$ javac JNIExample.java

다만 실행하려고하면 MyJNI 라이브러리를 찾지 못해서 다음과 같은 런타임 에러를 뱉을 것이다.

Exception in thread "main" java.lang.UnsatisfiedLinkError: no MyJNI in java.library.path
	at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1867)
	at java.lang.Runtime.loadLibrary0(Runtime.java:870)
	at java.lang.System.loadLibrary(System.java:1122)
	at JNIExample.<clinit>(JNIExample.java:3)

이제 JNI로 로드할 모듈을 구현해보자. C로 구현해 볼껀데 헤더 파일을 먼저 뽑아준다. javah 명령을 이용하면 된다. (javah 명령뒤에 '패키지명.클래스명'을 입력하면된다)

$ javah JNIExample

이 명령을 실행하면 다음 같은 JNIExample.h 파일이 생성된다. 

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class JNIExample */

#ifndef _Included_JNIExample
#define _Included_JNIExample
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     JNIExample
 * Method:    getNumber
 * Signature: ()I
 */
JNIEXPORT jint JNICALL Java_JNIExample_getNumber
  (JNIEnv *, jobject);

/*
 * Class:     JNIExample
 * Method:    printHelloWorld
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_JNIExample_printHelloWorld
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

이 헤더를 include해서 모듈을 구현해주면 된다. 이번에 구현할 모듈은 간단하게 숫자를 리턴하고, 

#include <stdio.h>
#include <jni.h>
#include "JNIExample.h"

JNIEXPORT jint JNICALL Java_JNIExample_getNumber (JNIEnv *, jobject) {
    return 17;
}

/*
 * Class:     JNIExample
 * Method:    printHelloWorld
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_JNIExample_printHelloWorld (JNIEnv *, jobject) {
    printf("Hello World\n");
}

코드의 내용은 정말 단순하다. 

이제 이 소스코드를 공유 라이브러리로 만들어줘야한다. 자바와 다르게 C로 구현된 소스코드는 배포될 환경에 맞게 컴파일되어야 한다. macOS에서는 다음과 같이 빌드하면 된다.

$ gcc -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/darwin/" -o libMyJNI.jnilib -shared MyJNI.c

이 명령을 실행하면 libMyJNI.dylib 파일이 생성된다. 주의할 점은 lib을 앞에다가 붙여줘야 JNI 로딩시 찾을 수 있다는 점이다. 이제 앞서 만들었던 JNIExample 클래스를 실행하면

$ java JNIExample
17
Hello World
$

 

이런 결과를 얻게 된다.

JNI 장점

  • 자바 언어에서 구현할 수 없거나 구현하기 힘든 기능들을 구현할 수 있다.
  • 하드웨어에 대한 제어를 쉽게 할 수 있다.
  • 기존 프로그램에서 자바 인터페이스를 제공하고 싶은 경우에 쓸 수 있다.
  • C/C++로 작성된 모듈은 빠르게 실행되기 때문에 성능 향상을 도모할 수 있다.

JNI 단점

  • 자바의 강점인 플랫폼 독립성(이식성)이 훼손될 수 있다.
  • 언어별 데이터형의 차이를 세세하게 수정해줘야한다.
  • 메모리 릭이 발생하면 답도 없다.

JNI를 사용할 경우 정적 테스트를 돌려보고, 메모리릭 테스트를 실행해봐야한다.

댓글