우리가 아는 것처럼 Java의 강점 중 하나는 이식성(플랫폼에 종속적이지 않다)이다, 컴파일을 하면 플랫폼 독립적인 바이트코드가 생성된다.
그러나 우리는 아래와 같은 이유로 특정 아키텍트를 위한 natively-compiled code(C, C++)를 사용해야할 때도 있다.
Java에서 제공하는 native는 메소드의 구현은 native 코드에서 제공된다는 것을 나타낸다.
보통 native 실행 프로그램을 만들 때 Static, Shared 두가지 라이브러리를 선택할 수 있다.
Static libs - 모든 라이브러리는 linking process(lib 사용을 위한 동작 일체로 이해)동안 실행파일의 일부로 포함된다, 따라서 사용하지 않아도 포함돼있기 때문에 용량을 증가시킨다.
Shared libs - 최종 실행파일 코드가 자체가 아니라 라이브러리를 참조만한다, 따라서 프로그램에서 사용되는 모든 파일에 접근할 수 있어야 한다.
하지만 bytecode와 natively-compiled code(C, C++)코드를 한 파일 넣을수는 없으므로 Share libs가 적합하다.
※ Static의 경우 파일안에 포함돼므로 사용하지 않아도 포함돼있어서 실행파일의 용량을 차지할 단점이 있지만 환경설정등의 번거로움은 없다, 반대로 Shared의 경우는 사용할때만 참조해서 쓰면 되기 때문에 실행파일등의 용량을 증가시키는 단점은 없지만 환경설정등의 작업을 하지 않으면 사용할 수 없는 단점이 있다 정도로만 이해함튜토리얼에서는 native 언어로는 C++을 컴파일과 linker로는 G++을 사용합니다.
G++의 설치는 O/S 마다 다른데
package my.example.jndistudy.example;
public class HelloWorldJNI {
static {
System.loadLibrary("native");
}
public static void main(String[] args) {
new HelloWorldJNI().sayHello();
}
private native void sayHello();
}
위에서 볼 수 있듯이 static 블럭 안에서 shared라이브러리를 로드했다, 이를 통해 언제 어디서나 사용할 수 있다.
다른곳에서 native라이브러리를 사용하지 않으므로, 네이티브 메소드를 호출하기 직전에 라이브러리를 로드할 수 있다.(확인 필요)
C++에서 native 메소드의 구현이 필요하다, C++에서 정의와 구현은 각각 .h, .cpp에 저장한다.
먼저 메소드를 정의하기 위해, 자바 컴파일러의 -h 옵션을 사용해야 한다
java 9 이전의 버전의 경우 -h 옵션이 아니라 javah 명령어를 사용해야 한다.
javac -h . TestJNI.java
위 명령을 실행하면 모든 native 메소드가 포함된 jni_TestJNI.h 파일이 생성된다
JNIEXPORT void JNICALL Java_jni_TestJNI_sayHello
(JNIEnv *, jobject);
메소드에 파라미터를 추가했을 경우 아래와 같이 jstring이 추가된다.
JNIEXPORT void JNICALL Java_jni_TestJNI_sayString
(JNIEnv *, jobject, jstring);
위에서 언급한 JNIEXPORT, JNICALL가 함께 사용한 경우이므로JNI에서 호출할 수 있다.
function 이름은 package, class, method 이름으로 자동으로 생성됐다.
또한 흥미로운 점은 현재 JNIEnv에 대한 포인터, 메소드가 현결된 java객체(즉 TestJNI 클래스의 인스턴스)가 전달됐다는 점이다.
다음은 .cpp파일(위에서 언급한 .h가 정의역할이라면 .cpp는 구현역할)을 만들어야 한다
JNIEXPORT void JNICALL Java_my_example_jndistudy_example_HelloWorldJNI_sayHello
(JNIEnv* env, jobject thisObject) {
std::cout << "Hello from C++ !!" << std::endl;
}
shared 라이브러리를 빌드하고 실행한다.
Ubuntu version
g++ -c -fPIC -I{JAVA_HOME}/include/linux my_example_jndistudy_example_HelloWorldJNI.cpp -o my_example_jndistudy_example_HelloWorldJNI.o
위의 내용을 추가한 .cpp 파일을 빌드하고 실행해야 하는데 g++ 컴파일러를 사용한다.
jdk설치에서 JNI 헤더를 포함해야 한다고 나와 있는데
위의 JAVA_HOME의 include 폴더를 보면 jni.h 파일이 있고 include/linux 폴더에는 jni_md.h 파일이 있다.
g++ -shared -fPIC -o libnative.so my_example_jndistudy_example_HelloWorldJNI.o -lc
G++ linker가 C++객체파일(my_example_jndistudy_example_HelloWorldJNI.o 파일)과 bridge라이브러리(libnative.so 파일)를 연결한다.
System.loadLibrary 인수를 native라 지정했으므로 libnative.so로 한다.
이대로 커맨드라인에서 프로그램을 실행시킬 수 있지만, 우리가 생성한 라이브러리를 포함하는 디렉토리의 경로를 추가해야 java가 native라이브러리의 경로를 알 수 있다.
이 부분에서 설명이 없어서 고생했는데 해당 레퍼런스처럼 위와 같이 실행하려면 패키지도 폴더로 생성되므로 my 폴더 상위에서 실행시켜야 한다. -Djava.library.path는 위에서 생성한 libnative.so 경로를 지정해주면 된다.java -cp . -Djava.library.path=/home/keikang/workspace/intellij-workspace/jndi-study/jndi-study/src/main/java/my/example/jndistudy/example my.example.jndistudy.example.HelloWorldJNI
intellij같은 IDE tool에서도 실행할 수 있는데 그럴경우 아래와 같이 VM 옵션에 해당값을 추가해주면 된다. 위와 같이 경로를 속 안 썩이고 제일 편한길이라 생각한다... 
아래와 같이 .cpp 파일에서 출력한 내용이 나온다면 성공이다.

private native long sumIntegers(int first, int second);
private native String sayHelloToMe(String name, boolean isFemale);
파라미터가 있는 native 메소드를 추가한다, 각각 long, String retutn 타입도 있다.
C++ 메소드를 만들기 위해 위의 javac -h 명령어로 .h 파일을 생성하고, .cpp파일에 .h 파일의 메소드(c에서는 function)를 구현한다.
JNIEXPORT jlong JNICALL Java_com_baeldung_jni_ExampleParametersJNI_sumIntegers
(JNIEnv* env, jobject thisObject, jint first, jint second) {
std::cout << "C++: The numbers received are : " << first << " and " << second << std::endl;
return (long)first + (long)second;
}
JNIEXPORT jstring JNICALL Java_com_baeldung_jni_ExampleParametersJNI_sayHelloToMe
(JNIEnv* env, jobject thisObject, jstring name, jboolean isFemale) {
const char* nameCharPointer = env->GetStringUTFChars(name, NULL);
std::string title;
if(isFemale) {
title = "Ms. ";
}
else {
title = "Mr. ";
}
std::string fullName = title + nameCharPointer;
return env->NewStringUTF(fullName.c_str());
}
위와 마찬가지로 g++ -c 커맨드를 이용하여 .o 파일을 생성, g++ -shared 커맨드를 이용하여 libnative.so 라이브러리 파일을 생성한다.
실행해서 위와 같은 결과가 나오면 성공이다.
sumIntegers function에 int 2개를 파라미터로 주고 sum을 long 값을 return 받아 출력했다.
sayHelloToMe function에서 String, boolean을 파라미터로 주고 String retrun값을 출력했다.
이번에는 Java Object를 native code에서 컨트롤 해보겠다. 아래와 같이 UserData라는 객체를 생성한다.
package com.baeldung.jni;
public class UserData {
public String name;
public double balance;
}
위의 java 객체 UserData를 return 하는, UserData를 파라미터로 받는 메소드 2개를 만든다.
public native UserData createUser(String name, double balance);
public native String printUserData(UserData user);
마찬가지로 javac -h로 .h 파일을 생성하는데 이때 주의할 점은
javac -h . HelloWorldJNI.java UserData.java
위 명령어처럼 참조하는 객체도 함께 써줘야 한다는 점이다.
그렇지 않으면 위와 같이 cannot find symbol을 경험할 수 있다.
가이드에서 이런부분을 빼놓고 설명하지 않는 이유를 모르겠다...
디버깅도 하나의 과제인건가?
그리고 지금까지와 마찬가지로 .cpp를 .o 파일로 .o를 shared 라이브러리로 등등을 수행하여 아래와 같은 출력이 나온다면 성공이다.
그러나 JNI에는 함정이 하나있다. 바로 플랫폼에 종속된다는 점이다, 지금 나의 경우에는 linux에서 했기때문에 linux에서만 실행가능하다, java 기본인 한번 작성하면 어디서나 실행할 수 있는 장점이 사라진다.
linux
-I${JAVA_HOME}/include/linux
windows
-I%JAVA_HOME%\include**win32**
위의 .cpp .o로 변환하는 명령어만 봐도 각각의 os종류에 따른 경로를 지정하는걸 알 수 있다.
심지어 marshaling/unmarshaling 프로세스에서는 java, native간의 데이터를 양방향으로 변환해야 한므로 JVM과 native 코드 간에 비용이 많이 드는 통신계층이 추가될수도 있다고 한다.
하지만 바이트코드를 실행하는 거 보다는 속도적으로 장점이 있는건 분명하므로 프로세스 속도를 높이려는 목적으로는 대안이 될 수 있다.
물론 그에 따라 아까 위에서 지적한 os별로 코드 버전관리를 해야한다는 단점도 같이 따라 온다. 따라서 이점을 고려하여 JNI 사용을 선택하는 것이 좋다고 한다.
code
github - AbstractRoutingDatasource-with-JPA
reference
https://www.baeldung.com/jni/