Java JNI를 사용하여 Windows 레지스트리 조작

Java 네이티브 인터페이스(JNI)를 활용하여 Windows 레지스트리에 접근하고 값을 읽고 쓰는 방법을 설명합니다. 이 예제는 64비트 Windows 환경에서 테스트되었으며, Java와 C 코드를 연동하여 네이티브 API를 호출하는 과정을 다룹니다.

프로젝트 구성

이 프로젝트는 다음과 같은 주요 파일로 구성됩니다:

  • Win32RegKey.java: 레지스트리 키 조작의 핵심 로직을 담고 있는 Java 클래스입니다. 내부적으로 Win32RegKeyExceptionWin32RegKeyNameEnumeration 클래스를 사용합니다.
  • Win32RegKey.c: JNI를 통해 Windows API와 통신하는 C/C++ 소스 코드입니다. Java 클래스의 네이티브 메서드에 대한 구현을 포함합니다.
  • Win32RegKeyTest.java: 레지스트리 쓰기 및 읽기 기능을 테스트하는 Java 클래스입니다.

JNI 헤더 파일 생성

JNI 인터페이스를 사용하기 위해서는 Java 네이티브 메서드에 대한 C/C++ 헤더 파일을 생성해야 합니다. 먼저 Java 코드를 컴파일합니다.

javac Win32RegKey.java

컴파일 후 생성된 `.class` 파일들을 사용하여 javah 명령어로 헤더 파일을 생성합니다. 패키지 구조에 따라 명령어가 달라질 수 있습니다.

src> javah Win32RegKey
src> javah Win32RegKeyNameEnumeration

만약 패키지가 com.example.jni와 같다면 다음과 같이 실행합니다.

src> javah com.example.jni.Win32RegKey

이 과정을 통해 Win32RegKey.hWin32RegKeyNameEnumeration.h 파일이 생성됩니다.

DLL 컴파일

생성된 헤더 파일과 C 소스 코드를 사용하여 Windows DLL 라이브러리를 컴파일합니다. GCC 컴파일러 환경이 필요하며, MinGW-64 (64비트) 또는 MinGW (32비트)를 사용할 수 있습니다.

다음은 64비트 DLL을 컴파일하는 예시 명령어입니다. JDK 경로(`jdk`)는 실제 설치 경로로 변경해야 합니다.

gcc -D __int64="long long" -Wl,--kill-at -I "C:/Program Files/Java/jdk-11.0.1/bin/../include" -I "C:/Program Files/Java/jdk-11.0.1/bin/../include/win32" -shared Win32RegKey.c -o Win32RegKey.dll
  • -D __int64="long long": MSVC의 __int64 타입에 대한 GNU 호환성을 설정합니다.
  • -I: JNI 헤더 파일의 include 경로를 지정합니다.
  • -shared: 공유 라이브러리(DLL) 생성을 지시합니다.

컴파일이 완료되면 생성된 Win32RegKey.dll 파일을 JDK의 jre/bin 디렉토리에 복사합니다. 예를 들어, C:/Program Files/Java/jdk-11.0.1/jre/bin 입니다.

Java 코드 예제

Win32RegKey.java

import java.util.Enumeration;

/**
 * Windows 레지스트리 키를 조작하기 위한 클래스입니다.
 */
public class Win32RegKey {
    /**
     * 레지스트리 키 객체를 생성합니다.
     *
     * @param rootKey HKEY_CLASSES_ROOT, HKEY_CURRENT_USER 등 루트 키 상수
     * @param keyPath 레지스트리 키 경로
     */
    public Win32RegKey(int rootKey, String keyPath) {
        this.root = rootKey;
        this.path = keyPath;
    }

    /**
     * 현재 레지스트리 키 아래의 모든 항목 이름을 열거합니다.
     *
     * @return 항목 이름 열거자
     */
    public Enumeration<String> names() {
        return new Win32RegKeyNameEnumeration(root, path);
    }

    /**
     * 지정된 이름의 레지스트리 값을 읽어옵니다.
     *
     * @param name 읽어올 값의 이름
     * @return 해당 값 (String, Integer, byte[] 등)
     */
    public native Object getValue(String name);

    /**
     * 지정된 이름의 레지스트리 값을 설정합니다.
     *
     * @param name 설정할 값의 이름
     * @param value 설정할 값 (String, Integer, byte[] 등)
     */
    public native void setValue(String name, Object value);

    // 레지스트리 루트 키 상수
    public static final int HKEY_CLASSES_ROOT = 0x80000000;
    public static final int HKEY_CURRENT_USER = 0x80000001;
    public static final int HKEY_LOCAL_MACHINE = 0x80000002;
    public static final int HKEY_USERS = 0x80000003;
    public static final int HKEY_CURRENT_CONFIG = 0x80000005;
    public static final int HKEY_DYN_DATA = 0x80000006;

    private int root;
    private String path;

    // 네이티브 라이브러리 로드
    static {
        System.loadLibrary("Win32RegKey");
    }
}

/**
 * 레지스트리 키의 항목 이름을 열거하는 클래스입니다.
 */
class Win32RegKeyNameEnumeration implements Enumeration<String> {
    Win32RegKeyNameEnumeration(int rootKey, String keyPath) {
        this.root = rootKey;
        this.path = keyPath;
    }

    public native String nextElement();
    public native boolean hasMoreElements();

    private int root;
    private String path;
    private int index = -1; // 열거 상태 추적
    private int hkey = 0;   // 네이티브 핸들
    private int maxSize;    // 최대 이름 길이
    private int count;      // 항목 수
}

/**
 * 레지스트리 작업 중 발생하는 예외를 위한 커스텀 런타임 예외입니다.
 */
class Win32RegKeyException extends RuntimeException {
    public Win32RegKeyException() {}
    public Win32RegKeyException(String message) {
        super(message);
    }
}

Win32RegKeyTest.java

import java.util.Enumeration;

/**
 * Win32RegKey 클래스의 기능을 테스트하는 메인 애플리케이션입니다.
 */
public class Win32RegKeyTest {

    public static void main(String[] args) {
        // HKEY_CURRENT_USER 아래의 특정 경로에 대한 Win32RegKey 객체 생성
        Win32RegKey regKey = new Win32RegKey(
                Win32RegKey.HKEY_CURRENT_USER, "Software\\JavaSoft\\Java Runtime Environment");

        // 레지스트리 값 설정
        regKey.setValue("DefaultUser", "TestUser");
        regKey.setValue("TestNumber", Integer.valueOf(123));
        regKey.setValue("BinaryData", new byte[]{0x01, 0x02, 0x03, 0x04});

        System.out.println("--- 레지스트리 항목 ---");

        // 모든 항목 이름 열거 및 값 읽기
        Enumeration<String> names = regKey.names();
        while (names.hasMoreElements()) {
            String name = names.nextElement();
            System.out.print(name + " = ");

            Object value = regKey.getValue(name);

            if (value instanceof byte[]) {
                // 바이트 배열의 경우 각 바이트를 16진수 형식으로 출력
                for (byte b : (byte[]) value) {
                    System.out.printf("%02X ", b);
                }
            } else {
                System.out.print(value);
            }
            System.out.println();
        }
        System.out.println("--------------------");
    }
}

C/C++ JNI 구현 (Win32RegKey.c)

이 C 코드는 Java 네이티브 메서드를 Windows 레지스트리 API와 연결합니다. getValue, setValue, 그리고 이름 열거를 위한 헬퍼 함수들을 구현합니다.

#include "Win32RegKey.h" // Java 클래스에 대응하는 헤더
#include "Win32RegKeyNameEnumeration.h" // 열거자 클래스 헤더
#include <string.h>
#include <stdlib.h>
#include <windows.h>

// 레지스트리 값 읽기 함수 구현
JNIEXPORT jobject JNICALL Java_Win32RegKey_getValue(JNIEnv* env, jobject thisObj, jstring valueName) {
    const char* cValueName;
    jstring keyPathJava;
    const char* cKeyPath;
    HKEY hKey;
    DWORD dataType;
    DWORD dataSize;
    jclass thisClass;
    jfieldID rootFieldID, pathFieldID;
    HKEY rootKey;
    jobject result;
    char* dataBuffer;

    // Java 객체의 클래스 및 필드 ID 가져오기
    thisClass = (*env)->GetObjectClass(env, thisObj);
    rootFieldID = (*env)->GetFieldID(env, thisClass, "root", "I");
    pathFieldID = (*env)->GetFieldID(env, thisClass, "path", "Ljava/lang/String;");

    // 필드 값 읽기
    rootKey = (HKEY)(*env)->GetIntField(env, thisObj, rootFieldID);
    keyPathJava = (jstring)(*env)->GetObjectField(env, thisObj, pathFieldID);
    cKeyPath = (*env)->GetStringUTFChars(env, keyPathJava, NULL);

    // 레지스트리 키 열기 (읽기 전용)
    if (RegOpenKeyEx(rootKey, cKeyPath, 0, KEY_READ, &hKey) != ERROR_SUCCESS) {
        (*env)->ThrowNew(env, (*env)->FindClass(env, "Win32RegKeyException"), "레지스트리 키 열기 실패");
        (*env)->ReleaseStringUTFChars(env, keyPathJava, cKeyPath);
        return NULL;
    }
    (*env)->ReleaseStringUTFChars(env, keyPathJava, cKeyPath);

    // 값 이름 UTF-8 문자열로 변환
    cValueName = (*env)->GetStringUTFChars(env, valueName, NULL);

    // 값의 타입과 크기 조회
    if (RegQueryValueEx(hKey, cValueName, NULL, &dataType, NULL, &dataSize) != ERROR_SUCCESS) {
        (*env)->ThrowNew(env, (*env)->FindClass(env, "Win32RegKeyException"), "레지스트리 값 정보 조회 실패");
        RegCloseKey(hKey);
        (*env)->ReleaseStringUTFChars(env, valueName, cValueName);
        return NULL;
    }

    // 값 데이터를 저장할 버퍼 할당
    dataBuffer = (char*)malloc(dataSize);
    if (dataBuffer == NULL) {
         (*env)->ThrowNew(env, (*env)->FindClass(env, "Win32RegKeyException"), "메모리 할당 실패");
         RegCloseKey(hKey);
         (*env)->ReleaseStringUTFChars(env, valueName, cValueName);
         return NULL;
    }

    // 실제 값 데이터 읽기
    if (RegQueryValueEx(hKey, cValueName, NULL, &dataType, (LPBYTE)dataBuffer, &dataSize) != ERROR_SUCCESS) {
        (*env)->ThrowNew(env, (*env)->FindClass(env, "Win32RegKeyException"), "레지스트리 값 읽기 실패");
        free(dataBuffer);
        RegCloseKey(hKey);
        (*env)->ReleaseStringUTFChars(env, valueName, cValueName);
        return NULL;
    }

    // 데이터 타입에 따라 Java 객체 생성
    if (dataType == REG_SZ) { // 문자열
        result = (*env)->NewStringUTF(env, dataBuffer);
    } else if (dataType == REG_DWORD) { // DWORD (정수)
        jclass integerClass = (*env)->FindClass(env, "java/lang/Integer");
        jmethodID constructorID = (*env)->GetMethodID(env, integerClass, "<init>", "(I)V");
        int intValue = *(int*)dataBuffer;
        result = (*env)->NewObject(env, integerClass, constructorID, intValue);
    } else if (dataType == REG_BINARY) { // 이진 데이터 (바이트 배열)
        result = (*env)->NewByteArray(env, dataSize);
        (*env)->SetByteArrayRegion(env, (jarray)result, 0, dataSize, (jbyte*)dataBuffer);
    } else { // 지원하지 않는 타입
        (*env)->ThrowNew(env, (*env)->FindClass(env, "Win32RegKeyException"), "지원하지 않는 값 타입");
        result = NULL;
    }

    free(dataBuffer);
    RegCloseKey(hKey);
    (*env)->ReleaseStringUTFChars(env, valueName, cValueName);

    return result;
}

// 레지스트리 값 설정 함수 구현
JNIEXPORT void JNICALL Java_Win32RegKey_setValue(JNIEnv* env, jobject thisObj,
    jstring valueName, jobject value) {

    const char* cValueName;
    jstring keyPathJava;
    const char* cKeyPath;
    HKEY hKey;
    DWORD dataType;
    DWORD dataSize;
    jclass thisClass;
    jfieldID rootFieldID, pathFieldID;
    HKEY rootKey;
    const char* stringData;
    int intData;
    const char* byteAreaData;

    // Java 객체 정보 가져오기
    thisClass = (*env)->GetObjectClass(env, thisObj);
    rootFieldID = (*env)->GetFieldID(env, thisClass, "root", "I");
    pathFieldID = (*env)->GetFieldID(env, thisClass, "path", "Ljava/lang/String;");
    rootKey = (HKEY)(*env)->GetIntField(env, thisObj, rootFieldID);
    keyPathJava = (jstring)(*env)->GetObjectField(env, thisObj, pathFieldID);
    cKeyPath = (*env)->GetStringUTFChars(env, keyPathJava, NULL);

    // 레지스트리 키 열기 (쓰기 가능)
    if (RegOpenKeyEx(rootKey, cKeyPath, 0, KEY_WRITE, &hKey) != ERROR_SUCCESS) {
        (*env)->ThrowNew(env, (*env)->FindClass(env, "Win32RegKeyException"), "레지스트리 키 열기 실패");
        (*env)->ReleaseStringUTFChars(env, keyPathJava, cKeyPath);
        return;
    }
    (*env)->ReleaseStringUTFChars(env, keyPathJava, cKeyPath);

    // 값 이름 UTF-8 문자열로 변환
    cValueName = (*env)->GetStringUTFChars(env, valueName, NULL);

    // 입력 값의 타입 판별
    jclass valueClass = (*env)->GetObjectClass(env, value);
    jclass stringClass = (*env)->FindClass(env, "java/lang/String");
    jclass integerClass = (*env)->FindClass(env, "java/lang/Integer");
    jclass byteArrayClass = (*env)->FindClass(env, "[B");

    if ((*env)->IsAssignableFrom(env, valueClass, stringClass)) { // String 타입
        stringData = (*env)->GetStringUTFChars(env, (jstring)value, NULL);
        dataType = REG_SZ;
        dataSize = (*env)->GetStringLength(env, (jstring)value) + 1; // null 종료 문자 포함
    } else if ((*env)->IsAssignableFrom(env, valueClass, integerClass)) { // Integer 타입
        jmethodID intValueMethodID = (*env)->GetMethodID(env, integerClass, "intValue", "()I");
        intData = (*env)->CallIntMethod(env, value, intValueMethodID);
        dataType = REG_DWORD;
        byteAreaData = (char*)&intData; // int 포인터를 char*로 캐스팅
        dataSize = sizeof(int);
    } else if ((*env)->IsAssignableFrom(env, valueClass, byteArrayClass)) { // byte[] 타입
        dataType = REG_BINARY;
        byteAreaData = (char*)(*env)->GetByteArrayElements(env, (jarray)value, NULL);
        dataSize = (*env)->GetArrayLength(env, (jarray)value);
    } else { // 지원하지 않는 타입
        (*env)->ThrowNew(env, (*env)->FindClass(env, "Win32RegKeyException"), "지원하지 않는 값 타입");
        RegCloseKey(hKey);
        (*env)->ReleaseStringUTFChars(env, valueName, cValueName);
        return;
    }

    // 레지스트리 값 설정
    if (RegSetValueEx(hKey, cValueName, 0, dataType, (const BYTE*)byteAreaData, dataSize) != ERROR_SUCCESS) {
        (*env)->ThrowNew(env, (*env)->FindClass(env, "Win32RegKeyException"), "레지스트리 값 설정 실패");
    }

    RegCloseKey(hKey);
    (*env)->ReleaseStringUTFChars(env, valueName, cValueName);

    // String 또는 byte[] 타입의 경우, GetByteArrayElements/GetStringUTFChars로 얻은 포인터 해제
    if (dataType == REG_SZ) {
        (*env)->ReleaseStringUTFChars(env, (jstring)value, stringData);
    } else if (dataType == REG_BINARY) {
        (*env)->ReleaseByteArrayElements(env, (jarray)value, (jbyte*)byteAreaData, 0);
    }
}

// 이름 열거를 위한 초기화 헬퍼 함수
static int initializeNameEnumeration(JNIEnv* env, jobject thisObj, jclass thisClass) {
    jfieldID indexFieldID, countFieldID, rootFieldID, pathFieldID, hkeyFieldID, maxSizeFieldID;
    HKEY rootKey, hKey;
    jstring keyPathJava;
    const char* cKeyPath;
    DWORD maxNameLen = 0;
    DWORD subKeyCount = 0;

    // 필드 ID 가져오기
    rootFieldID = (*env)->GetFieldID(env, thisClass, "root", "I");
    pathFieldID = (*env)->GetFieldID(env, thisClass, "path", "Ljava/lang/String;");
    hkeyFieldID = (*env)->GetFieldID(env, thisClass, "hkey", "I");
    maxSizeFieldID = (*env)->GetFieldID(env, thisClass, "maxSize", "I");
    indexFieldID = (*env)->GetFieldID(env, thisClass, "index", "I");
    countFieldID = (*env)->GetFieldID(env, thisClass, "count", "I");

    // 필드 값 가져오기
    rootKey = (HKEY)(*env)->GetIntField(env, thisObj, rootFieldID);
    keyPathJava = (jstring)(*env)->GetObjectField(env, thisObj, pathFieldID);
    cKeyPath = (*env)->GetStringUTFChars(env, keyPathJava, NULL);

    // 레지스트리 키 열기
    if (RegOpenKeyEx(rootKey, cKeyPath, 0, KEY_READ, &hKey) != ERROR_SUCCESS) {
        (*env)->ThrowNew(env, (*env)->FindClass(env, "Win32RegKeyException"), "레지스트리 키 열기 실패");
        (*env)->ReleaseStringUTFChars(env, keyPathJava, cKeyPath);
        return -1;
    }
    (*env)->ReleaseStringUTFChars(env, keyPathJava, cKeyPath);

    // 키 정보 조회 (하위 키 수, 최대 이름 길이 등)
    if (RegQueryInfoKey(hKey, NULL, NULL, NULL, NULL, NULL, NULL, &subKeyCount, &maxNameLen,
                        NULL, NULL, NULL) != ERROR_SUCCESS) {
        (*env)->ThrowNew(env, (*env)->FindClass(env, "Win32RegKeyException"), "레지스트리 키 정보 조회 실패");
        RegCloseKey(hKey);
        return -1;
    }

    // 열거자 객체의 필드 설정
    (*env)->SetIntField(env, thisObj, hkeyFieldID, (DWORD)hKey);
    (*env)->SetIntField(env, thisObj, maxSizeFieldID, maxNameLen + 1); // null 종료 문자 공간 포함
    (*env)->SetIntField(env, thisObj, indexFieldID, 0); // 인덱스 초기화
    (*env)->SetIntField(env, thisObj, countFieldID, subKeyCount); // 항목 수 설정

    return subKeyCount; // 총 항목 수 반환
}

// Win32RegKeyNameEnumeration.hasMoreElements 네이티브 메서드 구현
JNIEXPORT jboolean JNICALL Java_Win32RegKeyNameEnumeration_hasMoreElements(JNIEnv* env,
    jobject thisObj) {
    jclass thisClass = (*env)->GetObjectClass(env, thisObj);
    jfieldID indexFieldID = (*env)->GetFieldID(env, thisClass, "index", "I");
    jfieldID countFieldID = (*env)->GetFieldID(env, thisClass, "count", "I");
    int currentIndex = (*env)->GetIntField(env, thisObj, indexFieldID);

    if (currentIndex == -1) { // 첫 호출 시 초기화
        int totalCount = initializeNameEnumeration(env, thisObj, thisClass);
        currentIndex = 0; // 초기화 후 인덱스 0으로 설정
        if (totalCount == -1) return JNI_FALSE; // 초기화 실패 시
    } else {
        // 이미 초기화된 경우, count 필드 값 가져오기
        currentIndex = (*env)->GetIntField(env, thisObj, indexFieldID);
    }
    int totalCount = (*env)->GetIntField(env, thisObj, countFieldID);
    return currentIndex < totalCount;
}

// Win32RegKeyNameEnumeration.nextElement 네이티브 메서드 구현
JNIEXPORT jobject JNICALL Java_Win32RegKeyNameEnumeration_nextElement(JNIEnv* env,
    jobject thisObj) {

    jclass thisClass = (*env)->GetObjectClass(env, thisObj);
    jfieldID indexFieldID = (*env)->GetFieldID(env, thisClass, "index", "I");
    jfieldID countFieldID = (*env)->GetFieldID(env, thisClass, "count", "I");
    jfieldID hkeyFieldID = (*env)->GetFieldID(env, thisClass, "hkey", "I");
    jfieldID maxSizeFieldID = (*env)->GetFieldID(env, thisClass, "maxSize", "I");

    int currentIndex = (*env)->GetIntField(env, thisObj, indexFieldID);
    int totalCount = (*env)->GetIntField(env, thisObj, countFieldID);

    // 첫 호출 시 초기화 (hasMoreElements 에서 이미 처리되었을 수 있음)
    if (currentIndex == -1) {
        totalCount = initializeNameEnumeration(env, thisObj, thisClass);
        if (totalCount == -1) return NULL; // 초기화 실패
        currentIndex = 0;
    }

    // 열거 범위 초과 시 예외 발생
    if (currentIndex >= totalCount) {
        (*env)->ThrowNew(env, (*env)->FindClass(env, "java/util/NoSuchElementException"), "열거 범위를 벗어났습니다");
        return NULL;
    }

    DWORD currentMaxSize = (*env)->GetIntField(env, thisObj, maxSizeFieldID);
    HKEY hKey = (HKEY)(*env)->GetIntField(env, thisObj, hkeyFieldID);
    char* nameBuffer = (char*)malloc(currentMaxSize);
    if (nameBuffer == NULL) {
         (*env)->ThrowNew(env, (*env)->FindClass(env, "Win32RegKeyException"), "메모리 할당 실패");
         RegCloseKey(hKey); // 열린 핸들 닫기
         return NULL;
    }

    // 다음 레지스트리 항목 이름 조회
    // RegEnumValue는 이름의 실제 길이를 currentMaxSize에 저장함
    if (RegEnumValue(hKey, currentIndex, nameBuffer, ¤tMaxSize, NULL, NULL, NULL, NULL) != ERROR_SUCCESS) {
        (*env)->ThrowNew(env, (*env)->FindClass(env, "Win32RegKeyException"), "레지스트리 이름 열거 실패");
        free(nameBuffer);
        RegCloseKey(hKey);
        (*env)->SetIntField(env, thisObj, indexFieldID, totalCount); // 인덱스를 끝으로 이동시켜 hasMoreElements가 false 반환하도록 함
        return NULL;
    }

    jstring resultName = (*env)->NewStringUTF(env, nameBuffer);
    free(nameBuffer);

    // 인덱스 증가
    currentIndex++;
    (*env)->SetIntField(env, thisObj, indexFieldID, currentIndex);

    // 마지막 항목인 경우, 키 핸들 닫기
    if (currentIndex == totalCount) {
        RegCloseKey(hKey);
    }

    return resultName;
}

태그: JNI java Windows Registry Native

6월 30일 01:18에 게시됨