티스토리 뷰
보통 서로 다른 장, 단점을 갖고 있는 JAVA와 C++, 이 두 언어를 결합할 수만 있으면 큰 시너지를 발휘할 수 있다고 생각한다. 뭐, 어느 정도의 범위에서는 그렇긴 하지만...그러나 왠만하면, 정말 어쩔 수 없는 경우를 제외하고는 이 JNI 기술은 활용하지 않는 것이 최상이라고 난 생각한다. -_-;;
이제 마소에서 두 언어를 결합하는 방법을 전격 공개한다. 먼저 자바의 JNI를 이용해 C로 작성된 Win32 DLL을 호출하는 방법을 알아볼 텐데, 자바 코드에서 만든 데이터를 C 코드로 전달하거나 혹은 C 코드에서 생성된 데이터를 자바로 전달하는 방법, 한글 문제를 해결하는 방법 등도 포함돼 있다. 더불어 C/C++에서 자바 VM을 이용하는 방법도 함께 소개한다.
자바 네이티브 메쏘드(Java Native method, 이하 JNI)는 다른 언어로 작성된 코드를 자바에서 호출하도록 만들어진 규약이다. 현재는 C/C++에 대한 호출만을 정확하게 지원한다. 어떻게 보면 JNI는 자바가 만들어진 철학과 정반대되는 것이다. 네이티브 함수가 플랫폼에 종속적이기 때문이다. 자바의 가장 큰 장점 중의 하나로 내세우는 ‘플랫폼 독립적’이라는 부분을 해치는 JNI 규약이 만들어진 것은, 자바의 현실적인 문제들 때문이다.
먼저 속도 문제를 들 수 있다. JIT 컴파일러와 같은 기술로 자바 프로그램의 속도가 예전보다는 많이 빨라지고 있지만, 자바는 원칙적으로 바이트 코드를 인터프리트해 수행되기 때문에 아무리 자바 기술이 발전한다 해도, 네이티브 코드(플랫폼에 종속적인 기계어 코드)의 속도를 따라갈 수는 없다. 사실 자바의 처리 속도 문제는 자바가 안고 있는 가장 큰 단점이기도 하다. 따라서 아주 빠른 처리가 요구되는 대량의 계산 작업이나 실시간(real-time) 처리에는 자바를 이용하기 힘들다. - 이제 속도 문제는 신경을 안 써도 된다. 정말 속도가 중요시되는 전문 과학 분야를 제외하고는 JAVA의 처리 속도로도 거의 모든 문제를 해결할 수 있다.
JNI가 필요한 것은 단지 속도 문제만이 아니다. 기존에 작성된 프로그램이나 기존의 시스템(legacy)과의 연계 문제가 있으며, 더욱 중요한 이유는 플랫폼에 따라 다르게 제공되는 서비스를 이용할 수 있다는 점이다. 자바의 클래스 라이브러리는 방대하고 다양한 서비스를 제공하지만, 특정 플랫폼에서 제공하는 고유의 서비스의 기능을 모두 포함할 수는 없다. 특히, 특수한 목적으로 제작된 하드웨어를 자바에서 제어해야 할 필요가 있다고 한다면, 자바만으로 해결하기는 힘들다.
이 때문에 JNI가 필요하다. 지나치게 JNI에 의존하는 것은 자바가 갖고 있는 많은 장점들을 해치는 결과를 초래할 수 있지만, 특정 부분에 적절하게 사용한다면 JNI는 자바의 장점과 C/C++의 장점을 골고루 이용할 수 있는 길을 제공한다.
여기서 한 가지 궁금증이 생길 것이다. 그렇다면 과연 JNI를 어떤 부분에 사용해야 앞서 언급한 효과를 볼 수 있을까? JNI가 사용되면 좋은 곳을 정리해 보면 다음과 같다.
1. 속도 문제가 있는 계산 루틴
2. 자바에서 하드웨어 제어
3. 자바에서 지원되지 않은 특정 운영체제 서비스
4. 기존의 프로그램에서 자바가 제공하는 서비스를 이용
자바가 엔터프라이즈 솔루션에 많이 사용되는 것은 자바가 갖고 있는 많은 장점들이 대규모 시스템 구축에 적합하다는 강점을 인정받고 있는 셈이다. JNI는 그 자체가 중요한 솔루션이 될 수는 없지만, 기존의 코드나 기존의 시스템 혹은 이질적인 다른 시스템과의 인터페이스에 사용될 수 있는, 자바가 가진 좋은 기능 중의 하나이다.
★ 간단한 JNI 프로그래밍, HelloWorld
자바 프로그래밍을 한 번이라도 해 본 사람이라면, 다음과 같은 HelloWorld 프로그램을 작성해 보았을 것이다.
이제 이 프로그램을 JNI 버전으로 다시 만들어 보는 것부터 시작해 보려 한다. 가장 간단한 JNI 호출 방법과 컴파일 방법을 익히기 위해서이다
1단계 : 네이티브 메쏘드를 가진 클래스 작성
두 개의 자바 프로그램을 작성하고, 컴파일해 NativeHello.class, HelloWorld.class 파일을 생성한다.
NativeHello.java 프로그램은 C/C++로 작성된 Win32 네이티브 코드를 호출하기 위한 네이티브 메쏘드를 선언하는 부분이다.
SayHelloWorld는 자바에서 호출하는 메쏘드 이름이 되는데, ‘native’ 키워드를 이용해 선언하며, 메쏘드의 몸체(body)는 없다. 몸체는 C/C++ 코드로 컴파일된 DLL에 만들어진다. HelloWorld.Java는 NativeHello 객체를 만들어 SayHello World() 메쏘드를 호출하는 간단한 형태의 프로그램이다
2단계 : C 헤더 파일 생성
javah 명령을 써서 다음과 같이 헤더 파일을 생성한다. javah 명령은 JDK에 포함돼 있는 명령으로 .class 파일을 읽어, 여기에 들어있는 네이티브 메쏘드를 판독하고, 이를 C 헤더 파일로 생성해 낸다.
이 명령을 쓰려면 물론 JDK의 환경변수(특히 CLASSPATH 등)가 세팅돼 있어야 한다. 이렇게 하면 NativeHello.h라는 C 언어 헤더 파일이 생성된다. 그리고 옵션으로 jni를 주어야 한다. 물론 jni 옵션을 주지 않아도 헤더 파일이 생성되지만, JNI 스타일의 헤더가 아닐 것이기 때문이다. JDK 1.0 스타일의 헤더 파일을 생성하고자 한다면 -stubs 옵션을 준다.
생성된 헤더 파일인 NativeHello.h를 살펴보자. 이 파일은 다음과 같은 함수 선언을 포함하고 있다.
이 파일의 첫 부분에 ‘DO NOT EDIT THIS FILE’이란 말이 있다. 실제로 이 헤더 파일은 수정하지 말아야 한다. 이 Java_Native Hello_SayHelloWorld 함수 선언은 JNIEnv struct 포인터와 jobject 등 두 개의 인수를 갖고 있다. 이 두 개의 인수는 모든 JNI 호출에 추가된다.
첫번째 인수인 JNIEnv 포인터는 자바 가상머신(Virtual Machine, 이하 VM) 영역에 대한 포
인터이고, 두번째 jobject는 C++의 this에 해당한다. 즉, 클래스를 참조하는데 사용된다.
생성된 헤더 파일에서 ‘Signature:’ 부분도 눈여겨볼 필요가 있다. 현재 ‘V’라는 글자가 있는데 이것은 리턴 타입이 void란 의미이다.
javah 사용과 관련해 지정할 수 있는 옵션들은 다음과 같은 것들이 있다.
3단계 : 네이티브 메쏘드의 몸체가 될 C 코드 작성
1. 비주얼 C++에서 Win32 Dynamic-Link Library 타입으로 Native Hello라는 이름의 프로젝트를 생성한다.
2. 위저드의 1/1 단계에서 ‘A Simple Projcet’를 선택한다.
3. javah로 생성된 헤더 파일(NativeHello.h)을 프로젝트 디렉토리로 옮기고, NativeHello
.cpp 파일을 <리스트 4>와 같이 수정한다. 여기서는 헤더에서 생성된 함수의 이름을 그대로
구현하는 것이다.
4. Prject / Settings 메뉴를 선택한 뒤, <화면 1>과 같이 다음 인클루드 디렉토리를 추가
한다. 이 jni.h 파일은 네이티브 메쏘드를 위해 JDK가 제공하는 헤더 파일이다.
4단계 : 빌드하여 NativeHello.dll 파일을 생성하고 프로그램을 수행한다.
NativeHello.dll 파일을 NativeHello.class 파일이 있는 곳에 복사한다. 이렇게 하면 준비가 다 된 것이다. 이제 HelloWorld Java를 다음과 같이 수행한다.
<화면 2>는 이것의 수행된 결과이다. 간단한 메시지 박스가 나타난다. HelloWorld.java에서 NativeHello 클래스의 SayHelloWorld() 네이티브 메쏘드를 호출하는데, 이 네이티브 메쏘드의 몸체가 바로 NativeHello.cpp 파일의 Java_NativeHello_ SayHelloWorld() 함수가 되는 것이다.
이 함수는 간단히 MessageBox()란 Win32 API를 호출하는 내용으로 돼있다. DLL은 사실상 Win32의 실행 파일과 마찬가지로 모든 Win32 API를 사용할 수 있기 때문에 이론적으로는 JNI에서 모든 Win32 API를 이용할 수 있다.
★ 자바의 데이터 타입, C/C++의 데이터 타입
우리는 앞에서 JNI 프로그래밍의 전체적인 과정을 훑어보기 위해 가장 간단한 형태의 JNI 프로그램을 작성해 보았다. 사실 독자들이 가장 궁금해 할 내용은 아마도 자바 코드에서 만들어진 데이터를 C 코드로 전달하거나, 혹은 C 코드에서 생성된 데이터를 자바로 전달해주는 방법일 것이다. 일단 다음과 같이 String 타입의 객체를 인수로 전달하는 경우를 살펴보자.
이 코드를 컴파일한 후 javah로 C 헤더 파일을 생성하면 헤더에는 다음과 같은 내용의 함수
선언이 생성된다.
생성된 함수 선언을 보면, 모든 함수의 인수가 두 개 이상인 것을 확인할 수 있다. 처음 두
개의 인수(JNIEnv *, jobject)는 모든 JNI 함수에 생긴다. 세번째 인수부터가 우리가 전달
하고자 하는 인수이다.
JNI에는 C 언어와의 데이터 타입의 차이를 극복하기 위해 헤더에 ‘j’로 시작하는 새로운 타입을 재정의했다. 이 데이터 타입의 크기가 C와 같을 수도 있지만, 경우에 따라서는 다를 수도 있다.
예를 들어 Win32 환경의 경우 jint는 int와 같은 32비트 정수이다. 하지만 jchar은 char 타입과 다르다. 자바의 char은 16비트 정수이며, C의 char 타입은 8비트 정수이다. 또한 일반적으로 비주얼 C++에서 사용하는 BOOL은 int 타입을 재정의한 것이다. 따라서 자바의 boolean에 해당하는 jboolean 타입과는 크기가 다르다. 참고로 자바에서 true, false에 해당하는 부울 값은 에 다음과 같이 정의돼 있다.
이런 차이점에 유의해서 코드를 작성해야 한다(<표 1>).
★ UTF-8 문자열 다루기
자바 코드와 C 코드 간에는 jstring 타입으로 문자열을 주고받는데, jstring은 객체이기 때문에 C에서 ‘char *’와 같은 형태로 바로 이용할 수는 없다. 때문에 이를 위해 몇 가지 함수들이 제공된다.
우리는 두 가지 문제만 해결할 수 있으면 충분하다. 하나는 자바 코드에서 넘어온 jstring 타입을 C의 ‘char *’로 바꾸어 처리하는 방법과 C 코드에서 생성한 문자열을 자바로 리턴하는 방법이다. 먼저 자바에서 생성된 String 타입(jstring)의 객체를 C 코드에서 다루는 방법을 살펴보자.
자바에서는 아스키(ASCII) 문자열 이외에 UTF-8, UTF-16과 같은 문자열 포맷이 지원된다. 이것은 7비트로만 나타낼 수 없는 문자들(한글, 한자 등), 2바이트 이상으로 표현되는 문자들을 지원하기 위한 포맷이다. UTF-8(Universal Character Set Transforma tion Format, 8bit)은 8비트 즉, 바이트의 나열로 표시된다.
영문 아스키 문자의 경우는 일반적인 C의 문자열과 다르지 않다. 0x0001∼0x007F 범위의 문자는 1바이트를 사용하며, 0x0080~0x07FF 범위의 문자를 표현할 때는 2바이트가 사용된다.
첫번째 바이트는 이진수 110으로 시작하며, 두번째 바이트는 10으로 시작한다. 따라서 연속
된 두 바이트를 다음과 같이 조합해 하나의 유니코드 문자를 만들게 된다.
이와 비슷하게 0x0800∼0xFFFF 범위 사이의 문자 코드는 3개 바이트를 사용해 표현하며, 다음과 같이 연산해 하나의 유니코드 문자를 만들어 내게 된다. <표 2>는 UTF-8이 인코딩되는
방법을 보여주고 있다.
자바 VM이 사용하는 UTF-8 포맷에 대해 주의할 점이 있다. 첫번째는 널(null, 0x0000) 문자에 대한 표현이다. 0x0000 문자는 1바이트가 아닌 두 바이트로 표시된다. 즉, 0x0000 문자를 2진수 비트로 표시해 보면,
의 두 바이트로 나열된다. 따라서 C에서처럼 ‘\0(2진수 000 00000)’으로 끝나는 문자열이 아니라는 점을 알 수 있다. 만일 여러분이 jstring을 strcpy()와 같은 함수에 직접 사용한다면, ‘\0’ 문자가 없기 때문에 분명 프로그램이 죽을 것이다. 반대로 C의 문자열을 그대로 jstring으로 넘긴다면 자바 VM이 죽는다. jstring을 잘못 사용하는 예를 들어보자.
JNI는 jstring과 C의 문자열 처리를 위해 GetStringUTFChars (), ReleaseStringUTFChars() 함수를 제공한다.
GetString UTFChars() 함수는 UTF-8 포맷의 문자열을 널로 끝나는 C 스타일의 문자열로 전환해 포인터를 리턴하는 함수이다.
반대로 Relea seStringUTFChars() 함수는 원상태로 복구하는 함수이다. 덧붙여 C 스타일의 문자열을 UTF-8 문자열로 만들려면, New StringUTF() 함수를 사용한다. 다음은 C 문자열과 자바의 jstring 사이의 문제를 해결한 C++ 코드이다.
똑같은 내용이지만 C에서 호출한다면, 즉 확장자가 .c인 프로그램에서는 JNI 함수를 호출하는 방법이 약간 달라진다. 다음은 같은 내용의 C 코드이다.
이처럼, 같은 내용이라도 JNI 함수를 C와 C++에서 호출하는 방법에 차이가 있음을 알아두자.
이 내용들을 가지고 간단한 Win32 에디터를 만들어 보자.
먼저 <리스트 5>와 <리스트 6>에 있는 두 개의 자바 소스 파일을 만들고 각각을 컴파일한다. <리스트 5>의 WinDialgoBox.java 프로그램은 세 개의 네이티브 메쏘드를 선언하고 있는 클래스이며, 실제 이 네이티브 메쏘드의 몸체는 WinDialogBox.DLL 안에 만들어질 것이다. 그리
고 앞과 마찬가지로 자바 헤더 파일을 만든다.
이렇게 하면 WinDialogBox.h가 생성된다. 이 파일은 세 개의 네이티브 함수에 대한 선언을 포함하고 있다. 그리고 비주얼 C++에서 ‘WinDialogBox’란 프로젝트를 만든다(‘Win32 Dynamic-Link Library’ 타입으로 생성하며, 위저드의 첫 단계에서 ‘A Simple Projcet’를 체크한다). 그리고 javah 프로그램이 생성한 WinDialogBox.h 파일을 프로젝트에 포함시키고, WinDialogBox.cpp 파일을 <리스트 7>과 같이 편집한다.
이 C++ 프로그램의 내용을 잠깐 살펴보자. Java_WinDialog Box_doModal() 함수는 다이얼로그를 화면에 표시하는 함수이다. Java_WinDialogBox_setTextField()는 자바 코드에서 문자열을 넘겨 전역 문자 배열에 저장하는 함수이며, Java_WinDialogBox _getTextField() 함수는 전역 문자 배열의 내용을 jstring 타입으로 변환해 자바로 문자열을 리턴하는 함수이다.
DialgProc() 함수는 다이얼로그 프로시저이다. 이 다이얼로그 프로시저는 두 개의 메시지(WM_INITDIALOG, WM_COMMAND)를 처리하고 있다. 다이얼로그가 화면에 나타날 때, 즉 WM_INIT DIALOG 메시지가 전달되면 SetDlgItemText() 함수를 이용해 전역 문자열에 있던 문자를 에디트 컨트롤에 지정한다.
반대로, 버튼이 눌리면 GetDlgItemText() 함수를 이용해 편집된 문자열을 얻어와 전역 문자열에 저장한다. 이 저장된 값은 getTextField() 메쏘드를 통해 자바 프로그램으로 리턴되어 편집된 내용을 출력하게 된다.
이 C++ 코드가 생성하는 DLL은 리소스를 가지고 있다. 따라서 프로젝트에 리소스를 추가하고 다이얼로그 박스를 생성한다. 다이얼로그 박스는 <화면 3>과 같이 디자인한다. 이 다이얼로그 박스에는 IDC_EDIT_FIELD란 ID를 갖는 에디트 컨트롤이 포함돼 있다. 프로그램을 수행하면 <화면 4>와 같은 Win32 다이얼로그 박스가 나타난다. 여기에 입력된 내용은 다시 자바 프로그램으로 리턴돼 <화면 5>와 같이 도스창 화면에 출력된다.
지금까지 기본적인 데이터 타입인 jint, jstring 등의 데이터를 자바와 C 코드에서 넘겨주고 받는 방법들을 살펴보았다.
참고로 다음 코드는 자바에서 Win32 애플리케이션이 프로그램의 상태와 관련된 정보를 저장하기 위해 사용하는 Win32 API 함수 중의 하나인 GetPrivateProfileString()을 구현해본 것이다(레지스트리의 등장으로 요즘은 거의 사용하지 않지만). JNI는 이렇듯, 운영체제의 특징적인 서비스를 이용할 수 있는 장점이 있다.
주의 깊은 독자라면 이 프로그램을 테스트하면서 커다란 문제점을 발견할 수 있을 것이다. 그것은 바로 개발자들을 끊임없이 괴롭혀온 문제 중의 하나인 한글 문제이다.
자바에서 한글이 포함된 문자열을 C로 보내면(UTF-8 포맷으로), C에서는 이를 직접 사용할 수 없다. 또한 다이얼로그 박스에서 한글이 입력돼 자바로 전달하면 이것도 나타나지 않거나 ‘???’로 표시된다. 이런 문제는 왜 생기며, 어떻게 해결할 수 있을까?
★ JNI와 한글 문제
자바는 기본적으로 모든 문자열을 유니코드(Unicode)로 처리한다. 유니코드는 한 글자를 16비트로 표현하는 표준 코드로 바이트 문자열에 비해 크기가 커지는 단점이 있지만, 다양한 언어를 자유롭게 지원할 수 있다는 장점이 있다.
하지만 보통 Win32, C/C++ 환경에서 사용하는 문자열은 유니코드가 아닌 바이트 문자열(KSC5601)이다. 더구나 JNI에서 사용하는 jstring은 UTF-8 포맷으로 인코딩된 문자열이므로 직접 C 코드에서 사용하는 데는 어려움이 있다. 앞의 프로그램에서 해본 대로 영문자의 경우는 아무런 문제가 없다.
UTF-8 포맷이 영문자에 대해서는 1바이트로 인코딩되기 때문에 바이트 문자열과 UTF-8 포맷의 문자열은 사실상 같은 것이라고 보아도 무방하다. 하지만 한글의 경우는 다르다. 예를 들어보자. ‘한’이란 문자가 있다 하자. 이 글자는 <표 3>에서 볼 수 있듯 전혀 다른 형태로 저장된다.
만일 JNI를 통해 한글을 쓰고 싶다면, JNI를 통해 넘어온 jstring 문자열은 UTF-8 포맷이므로 이를 KSC5601 코드로 바꾸는 작업이 필요하다. 이 작업을 C 프로그램에서 해야 한다면, 사실 배보다 배꼽이 더 큰 일이다. 더욱이 UTF-8은 KSC5601 코드를 변환한 것이 아니라 유니코드를 인코딩한 것이므로 일은 더욱 복잡해진다.
자바에는 이 코드 변환에 대한 해결책이 이미 준비돼 있다. 따라서 이것을 C에서 해결하려 하기보다는 C에서는 단순하게 바이트 문자열만을 다루고, 유니코드와 UTF-8은 자바 코드에서 다루도록 하는 것이 프로그래밍을 훨씬 간단하게 한다.
즉, jstirng을 사용하지 않고, 바이트 배열을 주고받는 것이다. 경험으로 미루어 볼 때 자바와 C 간의 데이터 교환에서 가장 중요한 것은 사실 jstring보다 바이트 배열이다. 바이트 배열은 임의의 데이터 형태를 모두 전달할 수 있기 때문이다. 이것은 비단 JNI에만 국한된 문제는 아니다.
서블릿(sublet), 자바 애플리케이션(application), 애플릿(applet) 모두 외부에서 입력된 문자열을 처리해야 할 필요가 있기 때문에 자바 프로그래머라면 언제고 한번쯤은 부딪히게 되는 문제이다.
자바에서 외부에서 입력된 바이트 문자열을 자바 내부에서 사용하는 유니코드 혹은 UTF-8로 변환하는 것은 의외로 간단하다. <리스트 8>은 표준 입력에서 바이트 문자열을 입력받아 내부적으로 사용되는 String 객체로 다시 생성하는 방법을 보이고 있다.
System.in.read()를 통해 바이트 단위로 입력된 바이트 배열은 String 생성자를 통해 유니코드로 전환된다. String 생성자를 이용할 때, 바이트 배열이 인코딩된 방식을 지정해줄 수 있다.
이 생성자가 생성해낸 String 객체는 유니코드 문자열을 가지게 된다. 이 내용을 출력해 보면 똑같은 내용이 출력된다(<화면 6>). 자바에서 표준 출력에 출력할 때, 플랫폼 환경에 맞추어 다시 인코딩한 후 출력하기 때문이다.
이 두 개의 메쏘드는 String 클래스의 메쏘드로, 코드 변환에 유용하게 사용된다. enc에는 인코딩된 방식을 써주면 된다. 예를 들어 Win32 환경에서 가장 많이 사용하는 KSC5601 코드의 경우, 바이트 문자열을 유니코드로 전환하려면 다음과 같이 한다.
또한 유니코드를 KSC5601 바이트 문자열로 바꾸고자 한다면, 다음과 같이 해주면 된다.
앞에서 작성해 본 WinDialogBox.java 프로그램을 한글 처리를 염두에 두고 다시 작성해 보자. 파일 한글 처리가 문제되는 것은 외부에서 입력받는 내용이 유니코드 문자열이 아니라, 일반적으로 KSC5601 코드의 바이트 문자열이라는 점에 있다. 따라서 앞서 이야기한 KSC5601 바이트 코드와 유니코드 간의 변환을 이용하되, 자바 코드 외부에서 입력 혹은 전달되는 내용은 모두 바이트 배열로 처리하는 것이 바람직하다.
JNI에서는 <표 1>의 JNI에서 볼 수 있는 데이터 타입 이외에 다음과 같은 데이터 타입을 지
원한다.
객체나 각 기본 타입의 배열들에 대한 지원도 하고 있다. 여기서는 jbyteArray 타입을 이용할 것이다.
★ JNI에서 자바 배열 객체
자바의 배열은 자바의 객체이다. JNI를 통해 넘겨받은 자바의 배열을 다루기 위해 JNI가 제공하는 함수들에 대해 알아보자.
모든 자바 배열은 크기 필드를 갖고 있다. 다음 함수는 자바 배열 요소 개수를 알아내는 함
수이다.
배열의 각각 요소들을 다루기 위해서는 다음 JNI 함수가 필요하다.
이름에서도 알 수 있듯이 이 두 개의 함수는 배열의 요소를 얻거나 요소를 할당하는 함수이다. 배열에서 요소 값을 읽어올 때는 다음과 같은 GetArrayElements() 함수를 이용한다. 이 함수의 세번째 인자 isCopy는 자바 VM에서 배열의 요소들이 기억장소에 연속적으로 배치되어 있는지의 여부를 jboolean값으로 리턴한다. 이 값이 참(JNI_ TRUE)이라면 연속적으로 배
열된 복사본이라는 사실을 알 수 있고, 거짓(JNI_FALSE)이라면 실제 배열 요소를 다루어야
한다.
<표 4>는 GetArrayElements() 함수의 실제 함수 이름과 타입별 리턴 타입, 그리고 그에 해당하는 자바의 배열 타입을 정리한 것이다. 이 함수를 사용하고 나면, ReleaseArrayElements() 함수를 사용해 복사본을 기억장소에서 해지해야 한다. 이 함수의 실제 함수 이름과 리턴 타입도 <표 5>에 정리해 놓았다.
이 함수는 mode에 세 가지 다른 값을 지정할 수 있다.
이제 WinDialogBox의 두번째 버전(WinDialogBox2)을 만들어 보자. 이 프로그램을 만드는 방법은 앞서 보았던 WinDialogBox 프로그램을 만드는 방법과 꼭 같다. 자바 소스 프로그램을 <리스트 9>, <리스트 10>과 같이 만들고 컴파일한다. 그 다음, javah 명령을 이용해 WinDialogBox2.h 파일을 생성한다.
이제 DLL을 생성하기 위한 프로젝트로 WinDialogBox2를 새롭게 만든다. 여기에 다이얼로그 박스를 만들고, WinDialogBox2.cpp 프로그램을 <리스트 11>과 같이 작성한다. 빌드하여 만들어진 WinDialgoBox2.DLL 파일을 클래스 파일이 있는 곳에 복사하고, 다음과 같이 자바 프로그램을 수행시킨다.
<화면 7>은 TestWinDialogBox2 프로그램을 수행시킨 화면이다. 이제 한글 문제가 완전히 해
결됐다.
이처럼 JNI를 이용해 C 프로그램과 데이터 교환을 할 때 가장 유용하게 사용할 수 있는 것이 바이트 배열이다.
★ C/C++에서 자바 사용
지금까지 우리는 자바에서 C/C++ 코드를 사용하는 방법을 살펴보았다. 그렇다면 반대로 C/C++에서 VM을 이용하려면 어떻게 해야 할까? 이 부분은 자바에서 C 코드를 호출하는 것에 비해 상대적으로 중요하지 않기 때문에 자세하게 다루지는 않지만, 독자 여러분이 찾아볼 수 있을 정도의 간략한 개요를 정리해 보겠다.
C/C++에서 VM을 사용하기 위해 가장 먼저 해야 할 것은 C 코드에서 자바 VM을 생성하는 일이다. JNI는 C/C++ 애플리케이션에 자바 VM을 포함(embedding)할 수 있도록 지원한다. C/ C++ 애플리케이션에 VM을 생성하면, VM의 모든 클래스와 메쏘드를 이용할 수 있다.
환경에 대한 디폴트 설정 값을 얻어오기 위해 JNI_GetDefaultJavaVMInitArgs() 함수를 사용한다(JDK 1.1 이하). 이 함수는 JDK1_1InitArgs 구조체 포인터를 인자로 받는데, 이 구조체에는 다음과 같이 버전(vm_args.version) 값을 지정해 주어야 한다.
버전 값은 다음과 같이 정의돼 있다(JDK 1.2 이상). 버전으로 사용된 값 0x00010001의 의미는 1.1.2 이상되는 버전의 VM이면, 애플리케이션에서 사용할 수 있다는 의미가 된다.
그런데 이 JNI_GetDefaultJavaVMInitArgs() 함수는 JDK 1.2 이상에서는 쓰이지 않는다. C/C++에서 VM을 사용하려면 먼저 JNI_CreateJavaVM() 함수를 통해 VM을 생성한다.
이렇게 해 JVM이 생성되면, JNI 함수 중 FindClass()와 Call StaticVoidMethod()를 이용해 자바 클래스와 메쏘드를 호출한다. JVM 사용이 끝나면, DestroyJavaVM() 함수를 써서 VM을 삭제한다.
C/C++에서 대략 이런 과정을 통해 VM을 사용하게 된다. C/C++에서 자바 클래스를 이용하는 것이 실제로 많이 요구되지는 않는다.
지금까지 자바에서 C/C++ 코드를 호출하는데 필요한 내용들을 살펴보았다. 특히, 유니코드 관련 내용과 바이트 배열 처리 부분은 상당히 유용한 코드가 될 것이다.
이 JNI 코드 작성을 통해 우리는 몇 가지 사실을 알 수 있다. 우선, 자바에서 모든 Win32 호출을 이용할 수 있다는 점이다. 이것은 유닉스에서도 마찬가지이다. 모든 절차는 똑같으며, 단 유닉스에서는 DLL 대신 공유 라이브러리(shared library)를 사용한다는 점이 다르다.
이 글은 애플릿에서의 문제점은 다루고 있지는 않다(애플릿에서 JNI는 보안과 인증 문제를 갖고 있기 때문에 이 글에서 모두 다루기는 힘들다). C/C++로 액티브X 컴포넌트를 개발하는 프로그래머들이 비주얼 베이직 개발자들을 지원하듯, JNI는 C/C++ 개발자들이 자바 개발자들을 지원하는 창구 역할을 한다.
아무튼 자바에서 기존의 C/C++로 작성된 코드를 사용할 수 있고, 자바와 C/C++ 간의 데이터 교환이 가능하며, Win32 API를 모두 이용할 수 있다는 점은 JNI의 큰 매력이 아닐 수 없다.
이제 마소에서 두 언어를 결합하는 방법을 전격 공개한다. 먼저 자바의 JNI를 이용해 C로 작성된 Win32 DLL을 호출하는 방법을 알아볼 텐데, 자바 코드에서 만든 데이터를 C 코드로 전달하거나 혹은 C 코드에서 생성된 데이터를 자바로 전달하는 방법, 한글 문제를 해결하는 방법 등도 포함돼 있다. 더불어 C/C++에서 자바 VM을 이용하는 방법도 함께 소개한다.
자바 네이티브 메쏘드(Java Native method, 이하 JNI)는 다른 언어로 작성된 코드를 자바에서 호출하도록 만들어진 규약이다. 현재는 C/C++에 대한 호출만을 정확하게 지원한다. 어떻게 보면 JNI는 자바가 만들어진 철학과 정반대되는 것이다. 네이티브 함수가 플랫폼에 종속적이기 때문이다. 자바의 가장 큰 장점 중의 하나로 내세우는 ‘플랫폼 독립적’이라는 부분을 해치는 JNI 규약이 만들어진 것은, 자바의 현실적인 문제들 때문이다.
먼저 속도 문제를 들 수 있다. JIT 컴파일러와 같은 기술로 자바 프로그램의 속도가 예전보다는 많이 빨라지고 있지만, 자바는 원칙적으로 바이트 코드를 인터프리트해 수행되기 때문에 아무리 자바 기술이 발전한다 해도, 네이티브 코드(플랫폼에 종속적인 기계어 코드)의 속도를 따라갈 수는 없다. 사실 자바의 처리 속도 문제는 자바가 안고 있는 가장 큰 단점이기도 하다. 따라서 아주 빠른 처리가 요구되는 대량의 계산 작업이나 실시간(real-time) 처리에는 자바를 이용하기 힘들다. - 이제 속도 문제는 신경을 안 써도 된다. 정말 속도가 중요시되는 전문 과학 분야를 제외하고는 JAVA의 처리 속도로도 거의 모든 문제를 해결할 수 있다.
JNI가 필요한 것은 단지 속도 문제만이 아니다. 기존에 작성된 프로그램이나 기존의 시스템(legacy)과의 연계 문제가 있으며, 더욱 중요한 이유는 플랫폼에 따라 다르게 제공되는 서비스를 이용할 수 있다는 점이다. 자바의 클래스 라이브러리는 방대하고 다양한 서비스를 제공하지만, 특정 플랫폼에서 제공하는 고유의 서비스의 기능을 모두 포함할 수는 없다. 특히, 특수한 목적으로 제작된 하드웨어를 자바에서 제어해야 할 필요가 있다고 한다면, 자바만으로 해결하기는 힘들다.
이 때문에 JNI가 필요하다. 지나치게 JNI에 의존하는 것은 자바가 갖고 있는 많은 장점들을 해치는 결과를 초래할 수 있지만, 특정 부분에 적절하게 사용한다면 JNI는 자바의 장점과 C/C++의 장점을 골고루 이용할 수 있는 길을 제공한다.
여기서 한 가지 궁금증이 생길 것이다. 그렇다면 과연 JNI를 어떤 부분에 사용해야 앞서 언급한 효과를 볼 수 있을까? JNI가 사용되면 좋은 곳을 정리해 보면 다음과 같다.
2. 자바에서 하드웨어 제어
3. 자바에서 지원되지 않은 특정 운영체제 서비스
4. 기존의 프로그램에서 자바가 제공하는 서비스를 이용
자바가 엔터프라이즈 솔루션에 많이 사용되는 것은 자바가 갖고 있는 많은 장점들이 대규모 시스템 구축에 적합하다는 강점을 인정받고 있는 셈이다. JNI는 그 자체가 중요한 솔루션이 될 수는 없지만, 기존의 코드나 기존의 시스템 혹은 이질적인 다른 시스템과의 인터페이스에 사용될 수 있는, 자바가 가진 좋은 기능 중의 하나이다.
★ 간단한 JNI 프로그래밍, HelloWorld
자바 프로그래밍을 한 번이라도 해 본 사람이라면, 다음과 같은 HelloWorld 프로그램을 작성해 보았을 것이다.
public class HelloWorld {
public static void main(String arg[]) {
System.out.println("Hello, World");
}
}
이제 이 프로그램을 JNI 버전으로 다시 만들어 보는 것부터 시작해 보려 한다. 가장 간단한 JNI 호출 방법과 컴파일 방법을 익히기 위해서이다
1단계 : 네이티브 메쏘드를 가진 클래스 작성
두 개의 자바 프로그램을 작성하고, 컴파일해 NativeHello.class, HelloWorld.class 파일을 생성한다.
C:\jni>javac NativeHello.java
C:\jni>javac HelloWorld.java
C:\jni>javac HelloWorld.java
NativeHello.java 프로그램은 C/C++로 작성된 Win32 네이티브 코드를 호출하기 위한 네이티브 메쏘드를 선언하는 부분이다.
public native void SayHelloWorld();
SayHelloWorld는 자바에서 호출하는 메쏘드 이름이 되는데, ‘native’ 키워드를 이용해 선언하며, 메쏘드의 몸체(body)는 없다. 몸체는 C/C++ 코드로 컴파일된 DLL에 만들어진다. HelloWorld.Java는 NativeHello 객체를 만들어 SayHello World() 메쏘드를 호출하는 간단한 형태의 프로그램이다
2단계 : C 헤더 파일 생성
javah 명령을 써서 다음과 같이 헤더 파일을 생성한다. javah 명령은 JDK에 포함돼 있는 명령으로 .class 파일을 읽어, 여기에 들어있는 네이티브 메쏘드를 판독하고, 이를 C 헤더 파일로 생성해 낸다.
C:\jni>javah -jni NativeHello
이 명령을 쓰려면 물론 JDK의 환경변수(특히 CLASSPATH 등)가 세팅돼 있어야 한다. 이렇게 하면 NativeHello.h라는 C 언어 헤더 파일이 생성된다. 그리고 옵션으로 jni를 주어야 한다. 물론 jni 옵션을 주지 않아도 헤더 파일이 생성되지만, JNI 스타일의 헤더가 아닐 것이기 때문이다. JDK 1.0 스타일의 헤더 파일을 생성하고자 한다면 -stubs 옵션을 준다.
생성된 헤더 파일인 NativeHello.h를 살펴보자. 이 파일은 다음과 같은 함수 선언을 포함하고 있다.
JNIEXPORT void JNICALL Java_NativeHello_SayHelloWorld(JNIEnv *, jobject);
이 파일의 첫 부분에 ‘DO NOT EDIT THIS FILE’이란 말이 있다. 실제로 이 헤더 파일은 수정하지 말아야 한다. 이 Java_Native Hello_SayHelloWorld 함수 선언은 JNIEnv struct 포인터와 jobject 등 두 개의 인수를 갖고 있다. 이 두 개의 인수는 모든 JNI 호출에 추가된다.
첫번째 인수인 JNIEnv 포인터는 자바 가상머신(Virtual Machine, 이하 VM) 영역에 대한 포
인터이고, 두번째 jobject는 C++의 this에 해당한다. 즉, 클래스를 참조하는데 사용된다.
생성된 헤더 파일에서 ‘Signature:’ 부분도 눈여겨볼 필요가 있다. 현재 ‘V’라는 글자가 있는데 이것은 리턴 타입이 void란 의미이다.
javah 사용과 관련해 지정할 수 있는 옵션들은 다음과 같은 것들이 있다.
-o : 출력될 파일의 이름을 지정한다.
-d : 출력 디렉토리를 지정한다.
-jni : JNI 스타일의 헤더를 생성하도록 한다.
-td : 작업용 임시 디렉토리를 지정한다.
-stubs : stub 파일을 생성한다(JDK 1.0 스타일).
-trace : stub 파일에 추적 정보를 덧붙인다.
-verbose : 작업에 대한 자세한 정보를 출력한다.
-classpath : 클래스 경로를 지정한다. 디렉토리는 ‘;’으로 분리한다.
-version : 빌드 버전을 출력한다.
-d : 출력 디렉토리를 지정한다.
-jni : JNI 스타일의 헤더를 생성하도록 한다.
-td : 작업용 임시 디렉토리를 지정한다.
-stubs : stub 파일을 생성한다(JDK 1.0 스타일).
-trace : stub 파일에 추적 정보를 덧붙인다.
-verbose : 작업에 대한 자세한 정보를 출력한다.
-classpath : 클래스 경로를 지정한다. 디렉토리는 ‘;’으로 분리한다.
-version : 빌드 버전을 출력한다.
3단계 : 네이티브 메쏘드의 몸체가 될 C 코드 작성
1. 비주얼 C++에서 Win32 Dynamic-Link Library 타입으로 Native Hello라는 이름의 프로젝트를 생성한다.
2. 위저드의 1/1 단계에서 ‘A Simple Projcet’를 선택한다.
3. javah로 생성된 헤더 파일(NativeHello.h)을 프로젝트 디렉토리로 옮기고, NativeHello
.cpp 파일을 <리스트 4>와 같이 수정한다. 여기서는 헤더에서 생성된 함수의 이름을 그대로
구현하는 것이다.
4. Prject / Settings 메뉴를 선택한 뒤, <화면 1>과 같이 다음 인클루드 디렉토리를 추가
한다. 이 jni.h 파일은 네이티브 메쏘드를 위해 JDK가 제공하는 헤더 파일이다.
C:\jdk1.2.2\include,C:\jdk1.2.2\include\win32
4단계 : 빌드하여 NativeHello.dll 파일을 생성하고 프로그램을 수행한다.
NativeHello.dll 파일을 NativeHello.class 파일이 있는 곳에 복사한다. 이렇게 하면 준비가 다 된 것이다. 이제 HelloWorld Java를 다음과 같이 수행한다.
C:\jni>java HelloWorld
<화면 2>는 이것의 수행된 결과이다. 간단한 메시지 박스가 나타난다. HelloWorld.java에서 NativeHello 클래스의 SayHelloWorld() 네이티브 메쏘드를 호출하는데, 이 네이티브 메쏘드의 몸체가 바로 NativeHello.cpp 파일의 Java_NativeHello_ SayHelloWorld() 함수가 되는 것이다.
이 함수는 간단히 MessageBox()란 Win32 API를 호출하는 내용으로 돼있다. DLL은 사실상 Win32의 실행 파일과 마찬가지로 모든 Win32 API를 사용할 수 있기 때문에 이론적으로는 JNI에서 모든 Win32 API를 이용할 수 있다.
★ 자바의 데이터 타입, C/C++의 데이터 타입
우리는 앞에서 JNI 프로그래밍의 전체적인 과정을 훑어보기 위해 가장 간단한 형태의 JNI 프로그램을 작성해 보았다. 사실 독자들이 가장 궁금해 할 내용은 아마도 자바 코드에서 만들어진 데이터를 C 코드로 전달하거나, 혹은 C 코드에서 생성된 데이터를 자바로 전달해주는 방법일 것이다. 일단 다음과 같이 String 타입의 객체를 인수로 전달하는 경우를 살펴보자.
public class NativeClass {
public native void setString(String s);
public native void setInt(int i);
public native void setData(float f, double d);
...
}
이 코드를 컴파일한 후 javah로 C 헤더 파일을 생성하면 헤더에는 다음과 같은 내용의 함수
선언이 생성된다.
JNIEXPORT void JNICALL Java_NativeClass_setString(JNIEnv *, jobject, jstring);
JNIEXPORT void JNICALL Java_NativeClass_setInt(JNIEnv *, jobject, jint);
JNIEXPORT void JNICALL Java_NativeClass_setData(JNIEnv *, jobject, jfloat, jdouble);
JNIEXPORT void JNICALL Java_NativeClass_setInt(JNIEnv *, jobject, jint);
JNIEXPORT void JNICALL Java_NativeClass_setData(JNIEnv *, jobject, jfloat, jdouble);
생성된 함수 선언을 보면, 모든 함수의 인수가 두 개 이상인 것을 확인할 수 있다. 처음 두
개의 인수(JNIEnv *, jobject)는 모든 JNI 함수에 생긴다. 세번째 인수부터가 우리가 전달
하고자 하는 인수이다.
JNI에는 C 언어와의 데이터 타입의 차이를 극복하기 위해 헤더에 ‘j’로 시작하는 새로운 타입을 재정의했다. 이 데이터 타입의 크기가 C와 같을 수도 있지만, 경우에 따라서는 다를 수도 있다.
예를 들어 Win32 환경의 경우 jint는 int와 같은 32비트 정수이다. 하지만 jchar은 char 타입과 다르다. 자바의 char은 16비트 정수이며, C의 char 타입은 8비트 정수이다. 또한 일반적으로 비주얼 C++에서 사용하는 BOOL은 int 타입을 재정의한 것이다. 따라서 자바의 boolean에 해당하는 jboolean 타입과는 크기가 다르다. 참고로 자바에서 true, false에 해당하는 부울 값은 에 다음과 같이 정의돼 있다.
#define JNI_FALSE 0
#define JNI_TRUE 1
#define JNI_TRUE 1
이런 차이점에 유의해서 코드를 작성해야 한다(<표 1>).
<표 1> JNI에서 사용하는 데이터 타입과 범위 | |||
Java |
C(JNI) |
비트수 |
C/C++(Win32) |
boolean |
jboolean |
8 unsigned |
unsigned char |
byte |
jbyte |
8 |
unsigned char |
char |
jchar |
16 unsigned |
unsigned short |
short |
jshort |
16 |
short |
int |
jint |
32 |
int |
long |
jlong |
64 |
_int64 (비주얼C++) |
float |
jfloat |
32 |
float |
double |
jdouble |
64 |
double |
void |
void |
- |
void |
★ UTF-8 문자열 다루기
자바 코드와 C 코드 간에는 jstring 타입으로 문자열을 주고받는데, jstring은 객체이기 때문에 C에서 ‘char *’와 같은 형태로 바로 이용할 수는 없다. 때문에 이를 위해 몇 가지 함수들이 제공된다.
우리는 두 가지 문제만 해결할 수 있으면 충분하다. 하나는 자바 코드에서 넘어온 jstring 타입을 C의 ‘char *’로 바꾸어 처리하는 방법과 C 코드에서 생성한 문자열을 자바로 리턴하는 방법이다. 먼저 자바에서 생성된 String 타입(jstring)의 객체를 C 코드에서 다루는 방법을 살펴보자.
자바에서는 아스키(ASCII) 문자열 이외에 UTF-8, UTF-16과 같은 문자열 포맷이 지원된다. 이것은 7비트로만 나타낼 수 없는 문자들(한글, 한자 등), 2바이트 이상으로 표현되는 문자들을 지원하기 위한 포맷이다. UTF-8(Universal Character Set Transforma tion Format, 8bit)은 8비트 즉, 바이트의 나열로 표시된다.
영문 아스키 문자의 경우는 일반적인 C의 문자열과 다르지 않다. 0x0001∼0x007F 범위의 문자는 1바이트를 사용하며, 0x0080~0x07FF 범위의 문자를 표현할 때는 2바이트가 사용된다.
첫번째 바이트는 이진수 110으로 시작하며, 두번째 바이트는 10으로 시작한다. 따라서 연속
된 두 바이트를 다음과 같이 조합해 하나의 유니코드 문자를 만들게 된다.
((첫번째_바이트 & 0x1f)<<6) + (두번째_바이트 & 0x3f)
이와 비슷하게 0x0800∼0xFFFF 범위 사이의 문자 코드는 3개 바이트를 사용해 표현하며, 다음과 같이 연산해 하나의 유니코드 문자를 만들어 내게 된다. <표 2>는 UTF-8이 인코딩되는
방법을 보여주고 있다.
((첫번째_바이트 & 0xf)<<12)+((두번째_바이트 & 0x3f) <<6) + (세번째_바이트 & 0x3f)
<표 2> UTF-8 포맷의 문자열 인코딩 방식 | ||
유니코드(16진수) |
인코딩된 바이트 수 |
UTF-8로 인코딩된 비트(2진수) |
0x0001~0x007F |
1 |
0xxxxxxx |
0x0080~0x07FF |
2 |
110xxxxx 10xxxxxx |
0x0000 |
- |
- |
0x0800~0xFFFF |
3 |
1110xxxx 10xxxxxx 10xxxxxx |
자바 VM이 사용하는 UTF-8 포맷에 대해 주의할 점이 있다. 첫번째는 널(null, 0x0000) 문자에 대한 표현이다. 0x0000 문자는 1바이트가 아닌 두 바이트로 표시된다. 즉, 0x0000 문자를 2진수 비트로 표시해 보면,
11000000 10000000
의 두 바이트로 나열된다. 따라서 C에서처럼 ‘\0(2진수 000 00000)’으로 끝나는 문자열이 아니라는 점을 알 수 있다. 만일 여러분이 jstring을 strcpy()와 같은 함수에 직접 사용한다면, ‘\0’ 문자가 없기 때문에 분명 프로그램이 죽을 것이다. 반대로 C의 문자열을 그대로 jstring으로 넘긴다면 자바 VM이 죽는다. jstring을 잘못 사용하는 예를 들어보자.
JNIEXPORT void JNICALL Java_WinDialogBox_setTextField(JNIEnv *env, jobject obj, jstring str) {
char strBuff[128];
strcpy(strBuff, str) ; // C 모듈이 crash한다.
}
JNIEXPORT jstring JNICALL Java_WinDialogBox_getTextField(JNIEnv *env, jobject obj) {
char strBuff[128] = "Hello Java VM";
return strBuff; // VM이 crash한다.
}
char strBuff[128];
strcpy(strBuff, str) ; // C 모듈이 crash한다.
}
JNIEXPORT jstring JNICALL Java_WinDialogBox_getTextField(JNIEnv *env, jobject obj) {
char strBuff[128] = "Hello Java VM";
return strBuff; // VM이 crash한다.
}
JNI는 jstring과 C의 문자열 처리를 위해 GetStringUTFChars (), ReleaseStringUTFChars() 함수를 제공한다.
GetString UTFChars() 함수는 UTF-8 포맷의 문자열을 널로 끝나는 C 스타일의 문자열로 전환해 포인터를 리턴하는 함수이다.
반대로 Relea seStringUTFChars() 함수는 원상태로 복구하는 함수이다. 덧붙여 C 스타일의 문자열을 UTF-8 문자열로 만들려면, New StringUTF() 함수를 사용한다. 다음은 C 문자열과 자바의 jstring 사이의 문제를 해결한 C++ 코드이다.
JNIEXPORT void JNICALL Java_WinDialogBox_setTextField(JNIEnv *env, jobject obj, jstring str) {
char strBuff[128];
const char *sz = env->GetStringUTFChars(str, 0) ;
strcpy(strBuff, sz) ;
env->ReleaseStringUTFChars(str, sz) ;
}
JNIEXPORT jstring JNICALL Java_WinDialogBox_getTextField(JNIEnv *env, jobject obj) {
char strBuff[128] = "Hello Java VM";
return env->NewStringUTF(strBuff) ;
}
char strBuff[128];
const char *sz = env->GetStringUTFChars(str, 0) ;
strcpy(strBuff, sz) ;
env->ReleaseStringUTFChars(str, sz) ;
}
JNIEXPORT jstring JNICALL Java_WinDialogBox_getTextField(JNIEnv *env, jobject obj) {
char strBuff[128] = "Hello Java VM";
return env->NewStringUTF(strBuff) ;
}
똑같은 내용이지만 C에서 호출한다면, 즉 확장자가 .c인 프로그램에서는 JNI 함수를 호출하는 방법이 약간 달라진다. 다음은 같은 내용의 C 코드이다.
JNIEXPORT void JNICALL Java_WinDialogBox_setTextField(JNIEnv *env, jobject obj, jstring str) {
char strBuff[128];
const char *sz = (*env)->GetStringUTFChars(env, str, 0) ;
strcpy(strBuff, sz) ;
(*env)->ReleaseStringUTFChars(env, str, sz) ;
}
JNIEXPORT jstring JNICALL Java_WinDialogBox_getTextField(JNIEnv *env, jobject obj) {
char strBuff[128] = "Hello Java VM";
return (*env)->NewStringUTF(env, strBuff) ;
}
char strBuff[128];
const char *sz = (*env)->GetStringUTFChars(env, str, 0) ;
strcpy(strBuff, sz) ;
(*env)->ReleaseStringUTFChars(env, str, sz) ;
}
JNIEXPORT jstring JNICALL Java_WinDialogBox_getTextField(JNIEnv *env, jobject obj) {
char strBuff[128] = "Hello Java VM";
return (*env)->NewStringUTF(env, strBuff) ;
}
이처럼, 같은 내용이라도 JNI 함수를 C와 C++에서 호출하는 방법에 차이가 있음을 알아두자.
(*env)->FindClass(env, “java/lang/String”); // C의 경우
env->FindClass(“java/lang/String”); // C++의 경우
env->FindClass(“java/lang/String”); // C++의 경우
이 내용들을 가지고 간단한 Win32 에디터를 만들어 보자.
먼저 <리스트 5>와 <리스트 6>에 있는 두 개의 자바 소스 파일을 만들고 각각을 컴파일한다. <리스트 5>의 WinDialgoBox.java 프로그램은 세 개의 네이티브 메쏘드를 선언하고 있는 클래스이며, 실제 이 네이티브 메쏘드의 몸체는 WinDialogBox.DLL 안에 만들어질 것이다. 그리
고 앞과 마찬가지로 자바 헤더 파일을 만든다.
C:\jni>javah -jni WinDialogBox
이렇게 하면 WinDialogBox.h가 생성된다. 이 파일은 세 개의 네이티브 함수에 대한 선언을 포함하고 있다. 그리고 비주얼 C++에서 ‘WinDialogBox’란 프로젝트를 만든다(‘Win32 Dynamic-Link Library’ 타입으로 생성하며, 위저드의 첫 단계에서 ‘A Simple Projcet’를 체크한다). 그리고 javah 프로그램이 생성한 WinDialogBox.h 파일을 프로젝트에 포함시키고, WinDialogBox.cpp 파일을 <리스트 7>과 같이 편집한다.
이 C++ 프로그램의 내용을 잠깐 살펴보자. Java_WinDialog Box_doModal() 함수는 다이얼로그를 화면에 표시하는 함수이다. Java_WinDialogBox_setTextField()는 자바 코드에서 문자열을 넘겨 전역 문자 배열에 저장하는 함수이며, Java_WinDialogBox _getTextField() 함수는 전역 문자 배열의 내용을 jstring 타입으로 변환해 자바로 문자열을 리턴하는 함수이다.
DialgProc() 함수는 다이얼로그 프로시저이다. 이 다이얼로그 프로시저는 두 개의 메시지(WM_INITDIALOG, WM_COMMAND)를 처리하고 있다. 다이얼로그가 화면에 나타날 때, 즉 WM_INIT DIALOG 메시지가 전달되면 SetDlgItemText() 함수를 이용해 전역 문자열에 있던 문자를 에디트 컨트롤에 지정한다.
반대로, 버튼이 눌리면 GetDlgItemText() 함수를 이용해 편집된 문자열을 얻어와 전역 문자열에 저장한다. 이 저장된 값은 getTextField() 메쏘드를 통해 자바 프로그램으로 리턴되어 편집된 내용을 출력하게 된다.
이 C++ 코드가 생성하는 DLL은 리소스를 가지고 있다. 따라서 프로젝트에 리소스를 추가하고 다이얼로그 박스를 생성한다. 다이얼로그 박스는 <화면 3>과 같이 디자인한다. 이 다이얼로그 박스에는 IDC_EDIT_FIELD란 ID를 갖는 에디트 컨트롤이 포함돼 있다. 프로그램을 수행하면 <화면 4>와 같은 Win32 다이얼로그 박스가 나타난다. 여기에 입력된 내용은 다시 자바 프로그램으로 리턴돼 <화면 5>와 같이 도스창 화면에 출력된다.
지금까지 기본적인 데이터 타입인 jint, jstring 등의 데이터를 자바와 C 코드에서 넘겨주고 받는 방법들을 살펴보았다.
참고로 다음 코드는 자바에서 Win32 애플리케이션이 프로그램의 상태와 관련된 정보를 저장하기 위해 사용하는 Win32 API 함수 중의 하나인 GetPrivateProfileString()을 구현해본 것이다(레지스트리의 등장으로 요즘은 거의 사용하지 않지만). JNI는 이렇듯, 운영체제의 특징적인 서비스를 이용할 수 있는 장점이 있다.
jstring JNICALL Java_Win32Profile_GetPrivateProfileString(JNIEnv *jni, jclass, jstring section, jstring key, jstring defaut, jint maxLen, jstring name) {
char returned[4096];
LPCSTR file;
LPCSTR szSection;
LPCSTR szKey;
LPCSTR szDefault;
jint ret = 0;
file = jni->GetStringUTFChars(name, 0);
szSection = jni->GetStringUTFChars(section, 0);
szKey =jni->GetStringUTFChars(key, 0);
szDefault=jni->GetStringUTFChars(defaut, 0);
if (0 != strcmp(file, “win.ini”))
ret = GetPrivateProfileString(szSection, szKey, szDefault, returned, maxLen, file);
else
ret = GetProfileString(szSection, szKey, szDefault, returned, maxLen);
if (*returned == ‘\0’)
strcpy(returned, szDefault);
jni->ReleaseStringUTFChars(name, file);
jni->ReleaseStringUTFChars(section, szSection);
jni->ReleaseStringUTFChars(key, szKey);
jni->ReleaseStringUTFChars(defaut, szDefault);
return jni->NewStringUTF(returned);
}
char returned[4096];
LPCSTR file;
LPCSTR szSection;
LPCSTR szKey;
LPCSTR szDefault;
jint ret = 0;
file = jni->GetStringUTFChars(name, 0);
szSection = jni->GetStringUTFChars(section, 0);
szKey =jni->GetStringUTFChars(key, 0);
szDefault=jni->GetStringUTFChars(defaut, 0);
if (0 != strcmp(file, “win.ini”))
ret = GetPrivateProfileString(szSection, szKey, szDefault, returned, maxLen, file);
else
ret = GetProfileString(szSection, szKey, szDefault, returned, maxLen);
if (*returned == ‘\0’)
strcpy(returned, szDefault);
jni->ReleaseStringUTFChars(name, file);
jni->ReleaseStringUTFChars(section, szSection);
jni->ReleaseStringUTFChars(key, szKey);
jni->ReleaseStringUTFChars(defaut, szDefault);
return jni->NewStringUTF(returned);
}
주의 깊은 독자라면 이 프로그램을 테스트하면서 커다란 문제점을 발견할 수 있을 것이다. 그것은 바로 개발자들을 끊임없이 괴롭혀온 문제 중의 하나인 한글 문제이다.
자바에서 한글이 포함된 문자열을 C로 보내면(UTF-8 포맷으로), C에서는 이를 직접 사용할 수 없다. 또한 다이얼로그 박스에서 한글이 입력돼 자바로 전달하면 이것도 나타나지 않거나 ‘???’로 표시된다. 이런 문제는 왜 생기며, 어떻게 해결할 수 있을까?
★ JNI와 한글 문제
자바는 기본적으로 모든 문자열을 유니코드(Unicode)로 처리한다. 유니코드는 한 글자를 16비트로 표현하는 표준 코드로 바이트 문자열에 비해 크기가 커지는 단점이 있지만, 다양한 언어를 자유롭게 지원할 수 있다는 장점이 있다.
- 자바 : 유니코드(Unicode)
- JNI : UTF-8
- C/C++ : 바이트 문자열(KSC5601)
- JNI : UTF-8
- C/C++ : 바이트 문자열(KSC5601)
하지만 보통 Win32, C/C++ 환경에서 사용하는 문자열은 유니코드가 아닌 바이트 문자열(KSC5601)이다. 더구나 JNI에서 사용하는 jstring은 UTF-8 포맷으로 인코딩된 문자열이므로 직접 C 코드에서 사용하는 데는 어려움이 있다. 앞의 프로그램에서 해본 대로 영문자의 경우는 아무런 문제가 없다.
UTF-8 포맷이 영문자에 대해서는 1바이트로 인코딩되기 때문에 바이트 문자열과 UTF-8 포맷의 문자열은 사실상 같은 것이라고 보아도 무방하다. 하지만 한글의 경우는 다르다. 예를 들어보자. ‘한’이란 문자가 있다 하자. 이 글자는 <표 3>에서 볼 수 있듯 전혀 다른 형태로 저장된다.
만일 JNI를 통해 한글을 쓰고 싶다면, JNI를 통해 넘어온 jstring 문자열은 UTF-8 포맷이므로 이를 KSC5601 코드로 바꾸는 작업이 필요하다. 이 작업을 C 프로그램에서 해야 한다면, 사실 배보다 배꼽이 더 큰 일이다. 더욱이 UTF-8은 KSC5601 코드를 변환한 것이 아니라 유니코드를 인코딩한 것이므로 일은 더욱 복잡해진다.
자바에는 이 코드 변환에 대한 해결책이 이미 준비돼 있다. 따라서 이것을 C에서 해결하려 하기보다는 C에서는 단순하게 바이트 문자열만을 다루고, 유니코드와 UTF-8은 자바 코드에서 다루도록 하는 것이 프로그래밍을 훨씬 간단하게 한다.
즉, jstirng을 사용하지 않고, 바이트 배열을 주고받는 것이다. 경험으로 미루어 볼 때 자바와 C 간의 데이터 교환에서 가장 중요한 것은 사실 jstring보다 바이트 배열이다. 바이트 배열은 임의의 데이터 형태를 모두 전달할 수 있기 때문이다. 이것은 비단 JNI에만 국한된 문제는 아니다.
서블릿(sublet), 자바 애플리케이션(application), 애플릿(applet) 모두 외부에서 입력된 문자열을 처리해야 할 필요가 있기 때문에 자바 프로그래머라면 언제고 한번쯤은 부딪히게 되는 문제이다.
자바에서 외부에서 입력된 바이트 문자열을 자바 내부에서 사용하는 유니코드 혹은 UTF-8로 변환하는 것은 의외로 간단하다. <리스트 8>은 표준 입력에서 바이트 문자열을 입력받아 내부적으로 사용되는 String 객체로 다시 생성하는 방법을 보이고 있다.
System.in.read()를 통해 바이트 단위로 입력된 바이트 배열은 String 생성자를 통해 유니코드로 전환된다. String 생성자를 이용할 때, 바이트 배열이 인코딩된 방식을 지정해줄 수 있다.
이 생성자가 생성해낸 String 객체는 유니코드 문자열을 가지게 된다. 이 내용을 출력해 보면 똑같은 내용이 출력된다(<화면 6>). 자바에서 표준 출력에 출력할 때, 플랫폼 환경에 맞추어 다시 인코딩한 후 출력하기 때문이다.
String(byte[] bytes, String enc); // 바이트 문자열을 유니코드 문자열로 전환
byte[]getBytes(String enc); // 유니코드 문자열을 바이트 문자열로 전환
byte[]getBytes(String enc); // 유니코드 문자열을 바이트 문자열로 전환
이 두 개의 메쏘드는 String 클래스의 메쏘드로, 코드 변환에 유용하게 사용된다. enc에는 인코딩된 방식을 써주면 된다. 예를 들어 Win32 환경에서 가장 많이 사용하는 KSC5601 코드의 경우, 바이트 문자열을 유니코드로 전환하려면 다음과 같이 한다.
byte buffer [] = new byte[200];
...
String uni = new String(buffer, KSC5601);
...
String uni = new String(buffer, KSC5601);
또한 유니코드를 KSC5601 바이트 문자열로 바꾸고자 한다면, 다음과 같이 해주면 된다.
byte[] buffer;
String uni = “한글”;
buffer = uni.getBytes(KSC5601);
String uni = “한글”;
buffer = uni.getBytes(KSC5601);
앞에서 작성해 본 WinDialogBox.java 프로그램을 한글 처리를 염두에 두고 다시 작성해 보자. 파일 한글 처리가 문제되는 것은 외부에서 입력받는 내용이 유니코드 문자열이 아니라, 일반적으로 KSC5601 코드의 바이트 문자열이라는 점에 있다. 따라서 앞서 이야기한 KSC5601 바이트 코드와 유니코드 간의 변환을 이용하되, 자바 코드 외부에서 입력 혹은 전달되는 내용은 모두 바이트 배열로 처리하는 것이 바람직하다.
JNI에서는 <표 1>의 JNI에서 볼 수 있는 데이터 타입 이외에 다음과 같은 데이터 타입을 지
원한다.
struct _jobject;
typedef struct _jobject *jobject;
typedef jobject jclass;
typedef jobject jthrowable;
typedef jobject jstring;
typedef jobject jarray;
typedef jarray jbooleanArray;
typedef jarray jbyteArray;
typedef jarray jcharArray;
typedef jarray jshortArray;
typedef jarray jintArray;
typedef jarray jlongArray;
typedef jarray jfloatArray;
typedef jarray jdoubleArray;
typedef jarray jobjectArray;
typedef struct _jobject *jobject;
typedef jobject jclass;
typedef jobject jthrowable;
typedef jobject jstring;
typedef jobject jarray;
typedef jarray jbooleanArray;
typedef jarray jbyteArray;
typedef jarray jcharArray;
typedef jarray jshortArray;
typedef jarray jintArray;
typedef jarray jlongArray;
typedef jarray jfloatArray;
typedef jarray jdoubleArray;
typedef jarray jobjectArray;
객체나 각 기본 타입의 배열들에 대한 지원도 하고 있다. 여기서는 jbyteArray 타입을 이용할 것이다.
<표 3> ‘한’에 대한 코드 | ||
바이트 문자열(KSC5601) |
유니코드 |
UTF-8 |
0xC7 0xD1 |
0xD55C |
0xED 0x95 0x9C |
2바이트 |
16비트 코드 |
3바이트 |
★ JNI에서 자바 배열 객체
자바의 배열은 자바의 객체이다. JNI를 통해 넘겨받은 자바의 배열을 다루기 위해 JNI가 제공하는 함수들에 대해 알아보자.
모든 자바 배열은 크기 필드를 갖고 있다. 다음 함수는 자바 배열 요소 개수를 알아내는 함
수이다.
jsize GetArrayLength(JNIEnv *env, jarray array);
배열의 각각 요소들을 다루기 위해서는 다음 JNI 함수가 필요하다.
jobject GetObjectArrayElement(JNIEnv *env, jarray array, jsize index);
void SetObjectArrayElement(JNIEnv *env, jarray array, jsize index, jobject value);
void SetObjectArrayElement(JNIEnv *env, jarray array, jsize index, jobject value);
이름에서도 알 수 있듯이 이 두 개의 함수는 배열의 요소를 얻거나 요소를 할당하는 함수이다. 배열에서 요소 값을 읽어올 때는 다음과 같은 GetArrayElements() 함수를 이용한다. 이 함수의 세번째 인자 isCopy는 자바 VM에서 배열의 요소들이 기억장소에 연속적으로 배치되어 있는지의 여부를 jboolean값으로 리턴한다. 이 값이 참(JNI_ TRUE)이라면 연속적으로 배
열된 복사본이라는 사실을 알 수 있고, 거짓(JNI_FALSE)이라면 실제 배열 요소를 다루어야
한다.
NativeType GetArrayElements(JNIEnv *env, jarray array, jboolean *isCopy);
<표 4>는 GetArrayElements() 함수의 실제 함수 이름과 타입별 리턴 타입, 그리고 그에 해당하는 자바의 배열 타입을 정리한 것이다. 이 함수를 사용하고 나면, ReleaseArrayElements() 함수를 사용해 복사본을 기억장소에서 해지해야 한다. 이 함수의 실제 함수 이름과 리턴 타입도 <표 5>에 정리해 놓았다.
<표 4> 배열을 다루기 위한 JNI의 GetArrayElements 함수 | ||
JNI 함수 |
리턴타입 |
자바 배열 타입 |
GetBooleanArrayElements() |
jboolean * |
boolean[] |
GetByteArrayElements() |
jbyte * |
byte[] |
GetCharArrayElements() |
jchar * |
char[] |
GetShortArrayElements() |
jshort * |
short[] |
GetIntArrayElements() |
jint * |
int[] |
GetLongArrayElements() |
jlong * |
long[] |
GetFloatArrayElements() |
jfloat * |
float[] |
GetDoubleArrayElements() |
jdouble * |
double[] |
<표 5> 배열을 다루기 위한 JNI의 ReleaseArrayElements 함수 | ||
JNI 함수 |
네이티브 배열 타입 |
자바 배열 타입 |
ReleaseBooleanArrayElements() |
jboolean * |
boolean[] |
ReleaseByteArrayElements() |
jbyte * |
byte[] |
ReleaseCharArrayElements() |
jchar * |
char[] |
ReleaseShortArrayElements() |
jshort * |
short[] |
ReleaseIntArrayElements() |
jint * |
int[] |
ReleaseLongArrayElements() |
jlong * |
long[] |
ReleaseFloatArrayElements() |
jfloat * |
float[] |
ReleaseDoubleArrayElements() |
jdouble * |
double[] |
void ReleaseArrayElements(JNIEnv *env, jarray array, NativeType elems, jint mode);
이 함수는 mode에 세 가지 다른 값을 지정할 수 있다.
0 : 변경된 내용을 반영하고, 복사본의 기억장소를 해지한다.
JNI_COMMIT : 변경된 내용을 반영만 하고, 복사본의 기억장소를 해지하지 않는다.
JNI_ABORT : 변경된 내용을 반영하지 않고, 복사본의 기억장소를 해지한다.
JNI_COMMIT : 변경된 내용을 반영만 하고, 복사본의 기억장소를 해지하지 않는다.
JNI_ABORT : 변경된 내용을 반영하지 않고, 복사본의 기억장소를 해지한다.
이제 WinDialogBox의 두번째 버전(WinDialogBox2)을 만들어 보자. 이 프로그램을 만드는 방법은 앞서 보았던 WinDialogBox 프로그램을 만드는 방법과 꼭 같다. 자바 소스 프로그램을 <리스트 9>, <리스트 10>과 같이 만들고 컴파일한다. 그 다음, javah 명령을 이용해 WinDialogBox2.h 파일을 생성한다.
C:\jni> javac TestWinDialogBox2.java
C:\jni> javac WinDialgoBox2.java
C:\jni> javah jni WinDialgoBox2
C:\jni> javac WinDialgoBox2.java
C:\jni> javah jni WinDialgoBox2
이제 DLL을 생성하기 위한 프로젝트로 WinDialogBox2를 새롭게 만든다. 여기에 다이얼로그 박스를 만들고, WinDialogBox2.cpp 프로그램을 <리스트 11>과 같이 작성한다. 빌드하여 만들어진 WinDialgoBox2.DLL 파일을 클래스 파일이 있는 곳에 복사하고, 다음과 같이 자바 프로그램을 수행시킨다.
C:\jni> java TestWinDialogBox2
<화면 7>은 TestWinDialogBox2 프로그램을 수행시킨 화면이다. 이제 한글 문제가 완전히 해
결됐다.
이처럼 JNI를 이용해 C 프로그램과 데이터 교환을 할 때 가장 유용하게 사용할 수 있는 것이 바이트 배열이다.
★ C/C++에서 자바 사용
지금까지 우리는 자바에서 C/C++ 코드를 사용하는 방법을 살펴보았다. 그렇다면 반대로 C/C++에서 VM을 이용하려면 어떻게 해야 할까? 이 부분은 자바에서 C 코드를 호출하는 것에 비해 상대적으로 중요하지 않기 때문에 자세하게 다루지는 않지만, 독자 여러분이 찾아볼 수 있을 정도의 간략한 개요를 정리해 보겠다.
C/C++에서 VM을 사용하기 위해 가장 먼저 해야 할 것은 C 코드에서 자바 VM을 생성하는 일이다. JNI는 C/C++ 애플리케이션에 자바 VM을 포함(embedding)할 수 있도록 지원한다. C/ C++ 애플리케이션에 VM을 생성하면, VM의 모든 클래스와 메쏘드를 이용할 수 있다.
환경에 대한 디폴트 설정 값을 얻어오기 위해 JNI_GetDefaultJavaVMInitArgs() 함수를 사용한다(JDK 1.1 이하). 이 함수는 JDK1_1InitArgs 구조체 포인터를 인자로 받는데, 이 구조체에는 다음과 같이 버전(vm_args.version) 값을 지정해 주어야 한다.
JDK1_1InitArgs vm_args;
vm_args.version = 0x00010001;
JNI_GetDefaultJavaVMInitArgs (&vm_args);
vm_args.version = 0x00010001;
JNI_GetDefaultJavaVMInitArgs (&vm_args);
버전 값은 다음과 같이 정의돼 있다(JDK 1.2 이상). 버전으로 사용된 값 0x00010001의 의미는 1.1.2 이상되는 버전의 VM이면, 애플리케이션에서 사용할 수 있다는 의미가 된다.
#define JNI_VERSION_1_1 0x00010001
#define JNI_VERSION_1_2 0x00010002
#define JNI_VERSION_1_2 0x00010002
그런데 이 JNI_GetDefaultJavaVMInitArgs() 함수는 JDK 1.2 이상에서는 쓰이지 않는다. C/C++에서 VM을 사용하려면 먼저 JNI_CreateJavaVM() 함수를 통해 VM을 생성한다.
JavaVM *jvm;
JNIEnv *env;
jint rc = JNI_CreateJavaVM (&jvm, &env, &vm_args);
JNIEnv *env;
jint rc = JNI_CreateJavaVM (&jvm, &env, &vm_args);
이렇게 해 JVM이 생성되면, JNI 함수 중 FindClass()와 Call StaticVoidMethod()를 이용해 자바 클래스와 메쏘드를 호출한다. JVM 사용이 끝나면, DestroyJavaVM() 함수를 써서 VM을 삭제한다.
jvm-> DestroyJavaVM ()
C/C++에서 대략 이런 과정을 통해 VM을 사용하게 된다. C/C++에서 자바 클래스를 이용하는 것이 실제로 많이 요구되지는 않는다.
지금까지 자바에서 C/C++ 코드를 호출하는데 필요한 내용들을 살펴보았다. 특히, 유니코드 관련 내용과 바이트 배열 처리 부분은 상당히 유용한 코드가 될 것이다.
이 JNI 코드 작성을 통해 우리는 몇 가지 사실을 알 수 있다. 우선, 자바에서 모든 Win32 호출을 이용할 수 있다는 점이다. 이것은 유닉스에서도 마찬가지이다. 모든 절차는 똑같으며, 단 유닉스에서는 DLL 대신 공유 라이브러리(shared library)를 사용한다는 점이 다르다.
이 글은 애플릿에서의 문제점은 다루고 있지는 않다(애플릿에서 JNI는 보안과 인증 문제를 갖고 있기 때문에 이 글에서 모두 다루기는 힘들다). C/C++로 액티브X 컴포넌트를 개발하는 프로그래머들이 비주얼 베이직 개발자들을 지원하듯, JNI는 C/C++ 개발자들이 자바 개발자들을 지원하는 창구 역할을 한다.
아무튼 자바에서 기존의 C/C++로 작성된 코드를 사용할 수 있고, 자바와 C/C++ 간의 데이터 교환이 가능하며, Win32 API를 모두 이용할 수 있다는 점은 JNI의 큰 매력이 아닐 수 없다.
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
TAG
- 윈도우 제거된 디바이스 드라이버
- 멀티바이트 와이드 문자열 변경
- 시동 안걸릴때 여자들은?
- 코드 예시
- 대둔산
- 나는 아직 살아있는건가
- 공돌/공순 애인님 관리법
- 날자계산
- 랜 연결상태
- Don't let me be misunderstood
- D200
- 수락계곡
- 놈놈놈
- kde 전환
- Java PermGen eclipse 이클립스 메모리 부족
- Java
- Java Applet
- 18-70mm
- 정규식 Regex
- 엑스포다리
- 섹스와 남자들의 착각
- 자바스크립트
- ckeditor5 #custom image file insert #uploads
- gettimeofday
- Signed applet
- Mista Swing
- 남자는 키
- 2MB 개새끼
- spring-boot #java
- 대청호
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
글 보관함