본문 바로가기
Language/Java

[Java] Native Method

by 기몬식 2024. 6. 15.

얼마 전 Java 의 Thread 클래스 소스 코드를 들여다보던 중 native 키워드가 붙어 있는 메소드들을 발견했습니다. 특이한 점이 native 키워드를 포함하고 있는 메소드들은 그 구현체를 찾아볼 수 없었는데 도대체 이 메소드들은 어떻게 작동하는 것인지에 대한 궁금증이 생겼습니다. 따라서 이번 포스팅에서는 Java 의 Native Method 에 대해 알아 보도록 하겠습니다.

JNI

위에서 소개한 native 메소드를 알아 보기 전에 먼저 Java Native Interface(JNI)에 대해 알아야 합니다.


출처


JNI 는 Java 프로그램이 다른 언어로 작성된 프로그램과 상호 작용할 수 있게 해주는 인터페이스로 C 와 C++ 로 작성된 프로그램과의 상호작용을 위해 사용됩니다. 즉 JNI 는 C, C++ 처럼 인터프리터 없이 OS 가 바로 읽을 수 있는 형태의 네이티브 코드를 JVM 이 호출할 수 있게 하는 인터페이스입니다. 위키피디아의 JNI 내용에 따르면 JNI 는 다음과 같은 상황에서 유용하게 사용될 수 있다고 합니다.


  1. 표준 Java 클래스 라이브러리가 플랫폼별 기능이나 프로그램 라이브러리를 지원하지 않는 경우와 같이 애플리케이션을 Java 프로그래밍 언어로 완전히 작성할 수 없는 상황을 처리하는 기본 메소드를 작성할 수 있다.
  2. Java 애플리케이션에 액세스할 수 있도록 기존 애플리케이션(다른 프로그래밍 언어로 작성된)을 수정하는데 사용한다.

즉 JNI 는 일반적인 Java 코드를 작성하는 것과 동일한 방식으로 외부 함수를 손쉽게 사용할 수 있게끔 도와 주는 역할을 합니다.

Native Method

위에서 설명한 JNI 를 통해 Native 메소드는 Java 언어 외부에서 구현된 메소드를 Java 코드에서 호출하는 메소드를 의미합니다. native 키워드를 사용하여 선언되며 실제 구현은 Java가 아닌 C/C++ 로 작성됩니다. JVM 영역을 벗어나 운영 체제의 기능이나 기존의 네이티브 라이브러리에 접근할 수 있게 해줍니다.

예제

Java 에서 실제로 사용되고 있는 Native 메소드는 어떤 것이 있는지 Thread 클래스를 통해 알아 보도록 하겠습니다. Thread 클래스에는 다양한 native 메소드가 존재하지만 그 중 start() 메소드를 살펴 보면 다음과 같습니다.


public class Thread implements Runnable {
...
 public synchronized void start() {
           if (threadStatus != 0)
            throw new IllegalThreadStateException();

        group.add(this);

        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
            }
        }
    }

    private native void start0();
 ...   
}

start() 메소드는 실제로는 native 메소드인 start0() 메소드를 호출하는 것으로 구현되어 있습니다. JDK 의 구 스레드 모델은 KLT(kernel-level thread)와 ULT(user-level thread)를 1:1 매핑하여 사용하는데 이를 위해서는 운영 체제의 네이티브 API 를 호출해야 합니다. 따라서 start0() 메소드는 OS 가 바로 읽을 수 있는 형태의 native code 를 호출하기 위해 native 메소드를 사용하고 있는 예가 됩니다. 그렇다면 개발자가 직접 native 메소드를 정의해서 사용해봄으로써 JNI 와 natvie 메소드에 대해 좀 더 쉽게 이해할 수 있도록 실습을 해 보도록 하겠습니다.

실습

위에서 작성한 내용들을 기반으로 native 메소드를 직접 정의하고 실행해보는 실습을 진행하도록 하겠습니다.
먼저 외부 라이브러리를 읽어 오고 JNI 를 통해서 메소드를 실행하기 위한 클래스를 아래와 같이 작성합니다.


public class HelloJNI {
    static {
        System.loadLibrary("hello");
    }

    private native void sayHello();

    public static void main(String[] args) {
        new HelloJNI().sayHello();
    }
}

  1. static block 은 클래스 로더에 의해 로드될 때 한 번만 실행되는 코드 블록으로 네이티브 라이브러리를 로드하기 위한 코드를 작성합니다.
  2. native 메소드인 sayHello() 를 정의합니다. 해당 메소드는 JNI 를 사용하겠다는 예약어 입니다.
  3. main() 메소드에서 HelloJNI 인스턴스를 생성하여 sayHello() 를 호출합니다.

이제 준비는 끝났습니다. 실제로 native 메소드를 구현하기 위해 작성된 java 파일을 컴파일 하고 sayHello() 를 구현하기 위한 헤더 파일을 만듭니다.


javac HelloJNI

javah io.github.ones1kk.HelloJNI

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

#ifndef _Included_io_github_ones1kk_HelloJNI
#define _Included_io_github_ones1kk_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     io_github_ones1kk_HelloJNI
 * Method:    sayHello
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_io_github_ones1kk_HelloJNI_sayHello
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

생성된 헤더 파일을 포함하여 native 메소드를 구현합니다.


#include <jni.h>
#include "io_github_ones1kk_HelloJNI.h"

JNIEXPORT void JNICALL Java_io_github_ones1kk_HelloJNI_sayHello(JNIEnv *env, jobject obj) {  
       printf("Hello");
}

작성된 코드를 공유 라이브러리로 등록하기 위해 명령어를 입력합니다.


gcc -arch x86_64 -I$JAVA_HOME/include -I$JAVA_HOME/include/darwin -I"${헤더파일경로}" -shared -m64 ${경로}/${파일명}.c -o ${라이브러리명}.dylib

  • -I$JAVA_HOME/include: JAVA_HOME 환경 변수에 설정된 JDK 디렉토리의 include 디렉토리를 포함하도록 지정합니다. JNI 헤더 파일(jni.h)을 찾는 데 필요합니다.
  • -shared: 공유 라이브러리를 생성하고 해당 파일이 실행 파일이 아닌 공유 라이브러리임을 나타냅니다.
  • -o ${라이브러리명}.dylib: Output File 에 대한 옵션으로 생성될 동적 라이브러리의 이름과 경로를 지정합니다. Dynamic Library Design Guidelines 규약에 따라 lib${라이브러리명}.dylib 의 prefix, suffix 를 가집니다.
  • -arch x86_64: 애플 실리콘 Mac 으로 실습이 진행 되었기 때문에 타겟 아키텍처를 64비트(x86_64)로 지정합니다.

등록된 공유 라이브러리의 경로를 환경변수에 주입하여 main 메소드를 실행시킵니다.


java -Djava.library.path=${dylib파일경로}  -classpath ${classpath} ${패키지경로}.${자바파일명}


구현한 native 메소드가 정상적으로 실행되는 것을 확인할 수 있습니다. 즉 이렇게 JNI 를 사용하여 별도의 인터프리터 없이 C 로 작성된 코드를 실행하는 실습을 성공적으로 마무리했습니다.


오탈자 및 오류 내용을 댓글 또는 메일로 알려주시면, 검토 후 조치하겠습니다.

'Language > Java' 카테고리의 다른 글

[Java] 동기화  (0) 2024.03.13
[Java] 데이터 병렬 처리(Java 8)  (0) 2024.01.08
[Java] 데이터 병렬 처리(Java 5, 7)  (1) 2023.12.31
[Library] Assertions  (1) 2023.11.27
[Java] JPMS(Java 9 Platform Module System)  (1) 2023.10.23