1. Unity와 네이티브 플랫폼 간 상호작용의 필요성
Unity 프로젝트에서 안드로이드 네이티브 코드와의 상호작용이 필요한 경우는 주로 다음과 같습니다.
- 플랫폼 고유 기능 접근: Unity API만으로는 접근할 수 없는 안드로이드 운영체제 기능이나 하드웨어 자원(특정 센서, 블루투스 LE 상세 제어, 배터리 상태 상세 정보 등)을 사용해야 할 때.
- 서드파티 안드로이드 SDK 통합: 광고, 분석, 소셜 미디어, 결제 등 다양한 목적의 안드로이드 네이티브 SDK를 Unity 프로젝트에 통합하고 해당 SDK의 기능을 호출해야 할 때.
- 기존 네이티브 코드 재사용: 이미 Java나 Kotlin으로 작성된 라이브러리나 비즈니스 로직을 Unity 프로젝트에서 재활용하고 싶을 때.
- 플랫폼 UI/UX 활용: 안드로이드 시스템이 제공하는 표준 UI 요소(토스트 메시지, 알림, 다이얼로그 등)나 시스템 상호작용 기능을 활용하여 사용자 경험을 향상시키고 싶을 때.
2. AndroidJavaClass
: 네이티브 Java/Kotlin 클래스 접근
UnityEngine.AndroidJavaClass
는 C# 코드에서 Java 클래스 자체를 표현하는 프록시(Proxy) 객체입니다. 주로 다음과 같은 목적으로 사용됩니다.
- 정적(Static) 멤버 접근: 해당 Java 클래스의 정적 필드(Static Field) 값을 읽거나 쓰거나, 정적 메서드(Static Method)를 호출합니다.
- 객체 생성: 해당 Java 클래스의 생성자(Constructor)를 호출하여 새로운 Java 객체 인스턴스(결과적으로
AndroidJavaObject
로 표현됨)를 생성합니다.
[생성 방법]
// new AndroidJavaClass("패키지명을 포함한 전체 Java 클래스 이름");
AndroidJavaClass buildClass = new AndroidJavaClass("android.os.Build");
AndroidJavaClass unityPlayerClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
AndroidJavaClass customPluginClass = new AndroidJavaClass("com.mycompany.myplugin.MyPluginClass"); // 직접 만든 플러그인 클래스
생성자에는 접근하려는 Java 클래스의 정규화된 이름(Fully Qualified Name) 을 문자열로 전달해야 합니다.
3. AndroidJavaObject
: 네이티브 Java/Kotlin 객체(인스턴스) 접근
UnityEngine.AndroidJavaObject
는 C# 코드에서 Java 클래스의 인스턴스(객체) 를 표현하는 프록시 객체입니다. 주로 다음과 같은 목적으로 사용됩니다.
- 인스턴스(Instance) 멤버 접근: 해당 Java 객체의 인스턴스 필드 값을 읽거나 쓰거나, 인스턴스 메서드를 호출합니다.
[생성 방법]
AndroidJavaClass
를 통해 생성자 호출:// new AndroidJavaObject("패키지명을 포함한 전체 클래스 이름", 생성자 인자1, 인자2, ...); AndroidJavaObject customObject = new AndroidJavaObject("com.mycompany.myplugin.MyDataObject", 10, "initial data");
- 정적 메서드 또는 필드로부터 객체 참조 얻기:
AndroidJavaClass unityPlayerClass = new AndroidJavaClass("com.unity3d.player.UnityPlayer"); // currentActivity는 UnityPlayer 클래스의 정적 필드(또는 메서드)로, 현재 실행 중인 Activity 객체를 반환 AndroidJavaObject currentActivity = unityPlayerClass.GetStatic<AndroidJavaObject>("currentActivity");
- 다른
AndroidJavaObject
의 메서드 호출 결과로 얻기:AndroidJavaObject someManager = GetSomeManagerInstance(); // 다른 방법으로 얻은 AJO AndroidJavaObject subObject = someManager.Call<AndroidJavaObject>("getSubObject");
4. 정적(Static) 멤버 호출 및 접근
Java 클래스의 정적 멤버에 접근하거나 호출하려면 AndroidJavaClass
인스턴스를 사용합니다.
- 정적 메서드 호출:
- 반환 값이 있는 경우:
CallStatic<ReturnType>(string methodName, params object[] args)
- 반환 값이 없는 경우(
void
):CallStatic(string methodName, params object[] args)
AndroidJavaClass systemClass = new AndroidJavaClass("java.lang.System"); long currentTimeMillis = systemClass.CallStatic<long>("currentTimeMillis");
- 반환 값이 있는 경우:
- 정적 필드 접근:
- 값 읽기:
GetStatic<FieldType>(string fieldName)
- 값 쓰기:
SetStatic(string fieldName, FieldType value)
AndroidJavaClass buildClass = new AndroidJavaClass("android.os.Build"); string deviceModel = buildClass.GetStatic<string>("MODEL"); // 정적 필드 MODEL 값 읽기 Debug.Log("Device Model: " + deviceModel);
- 값 읽기:
5. 인스턴스 멤버 호출 및 접근
Java 객체의 인스턴스 멤버에 접근하거나 호출하려면 AndroidJavaObject
인스턴스를 사용합니다.
- 인스턴스 메서드 호출:
- 반환 값이 있는 경우:
Call<ReturnType>(string methodName, params object[] args)
- 반환 값이 없는 경우(
void
):Call(string methodName, params object[] args)
// currentActivity는 이전 예제에서 얻은 AndroidJavaObject라고 가정 AndroidJavaObject context = currentActivity.Call<AndroidJavaObject>("getApplicationContext"); string packageName = context.Call<string>("getPackageName"); Debug.Log("Package Name: " + packageName);
- 반환 값이 있는 경우:
- 인스턴스 필드 접근:
- 값 읽기:
Get<FieldType>(string fieldName)
- 값 쓰기:
Set(string fieldName, FieldType value)
// 예시: 네이티브 플러그인의 객체 필드 접근 (가정) // AndroidJavaObject myPluginInstance = ...; // int currentScore = myPluginInstance.Get<int>("score"); // myPluginInstance.Set("playerName", "NewPlayerName");
- 값 읽기:
6. 데이터 타입 변환 및 제약사항
C#과 Java/Kotlin 간의 데이터를 주고받을 때는 타입 변환 규칙과 몇 가지 제약사항을 이해해야 합니다.
- 기본 타입 (Primitive Types):
int
,long
,float
,double
,bool
,byte
,short
,char
, 그리고string
과 같은 기본 타입들은 대부분 C#과 Java 간에 자동으로 호환/변환됩니다. - 배열 (Arrays): 기본 타입의 1차원 배열(
int[]
,float[]
,string[]
등)은 종종 직접 전달될 수 있습니다. - 복합 타입 / 사용자 정의 클래스: C# 클래스 객체를 Java 메서드에 직접 전달하거나 그 반대는 간단하지 않습니다. 보통 다음과 같은 방법을 사용합니다.
- C# 측에서는 Java 객체를
AndroidJavaObject
로 받거나 전달합니다. - 데이터를 JSON과 같은 공통 형식으로 직렬화(Serialize)하여 문자열로 전달하고, 각 플랫폼에서 역직렬화(Deserialize)합니다.
- 양쪽 플랫폼에서 공통으로 이해할 수 있는 인터페이스를 정의하고, C#에서는
AndroidJavaProxy
를 사용하여 해당 인터페이스를 구현한 객체를 Java 측에 전달합니다 (콜백 구현 시 유용).
- C# 측에서는 Java 객체를
- 콜백 (C# 메서드를 Java에서 호출): Java 코드에서 C#의 특정 메서드를 호출해야 하는 경우(예: 비동기 작업 완료 알림), C#에서
AndroidJavaProxy
클래스를 사용하여 Java 인터페이스를 구현하고 이 프록시 객체를 Java 코드에 전달하는 방식을 사용합니다. 이는 다소 고급 기법입니다. - JNI 오버헤드:
AndroidJavaClass
와AndroidJavaObject
를 통한 모든 호출은 내부적으로 JNI(Java Native Interface) 호출을 거칩니다. 이는 순수 C# 코드 실행보다 성능 오버헤드가 발생합니다. 따라서Update()
와 같은 매우 빈번하게 호출되는 루프 내에서 JNI 호출을 남용하는 것은 피해야 합니다. 필요하다면 여러 번 호출할 내용을 네이티브 플러그인 내에서 하나의 메서드로 묶어 호출 횟수를 줄이는 것이 좋습니다. - 스레딩: Unity에서 이들 클래스를 사용하여 네이티브 코드를 호출하면, 일반적으로 호출이 발생한 스레드(대부분 Unity의 메인 스레드)에서 네이티브 코드가 실행됩니다. 만약 호출된 네이티브 코드가 오래 실행되거나 블로킹(Blocking)되면 Unity 메인 스레드도 함께 멈추게 됩니다. 안드로이드 UI 관련 작업(예: 토스트 메시지 표시)은 반드시 안드로이드의 UI 스레드에서 실행되어야 하므로, Unity 메인 스레드에서 직접 호출하면 안 됩니다 (
Activity.runOnUiThread
사용 필요 - 아래 예시 참고).
7. 코드 예시
예시 1: 안드로이드 기기 모델명 가져오기 (정적 호출)
using UnityEngine;
public class DeviceInfo : MonoBehaviour
{
void Start()
{
string deviceModel = GetAndroidDeviceModel();
Debug.Log("Device Model (from C#): " + deviceModel);
}
string GetAndroidDeviceModel()
{
// 안드로이드 플랫폼에서만, 그리고 에디터가 아닐 때만 실행
#if UNITY_ANDROID && !UNITY_EDITOR
try
{
// android.os.Build 클래스 접근
AndroidJavaClass buildClass = new AndroidJavaClass("android.os.Build");
// 정적 필드 "MODEL" 값 가져오기
string model = buildClass.GetStatic<string>("MODEL");
return model;
}
catch (System.Exception e)
{
Debug.LogError("Error getting Android model: " + e.Message);
return "Error fetching model";
}
#else
// 에디터나 다른 플랫폼에서는 대체 값 반환
return "Not running on Android device";
#endif
}
}
예시 2: 안드로이드 토스트 메시지 표시 (인스턴스 호출 및 UI 스레드 처리)
using UnityEngine;
public class NativeToast : MonoBehaviour
{
public void ShowToastMessage(string message)
{
// 안드로이드 플랫폼에서만, 그리고 에디터가 아닐 때만 실행
#if UNITY_ANDROID && !UNITY_EDITOR
// 현재 실행 중인 Activity 객체 가져오기
AndroidJavaClass unityPlayer = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
AndroidJavaObject currentActivity = unityPlayer.GetStatic<AndroidJavaObject>("currentActivity");
// Toast 표시는 UI 스레드에서 실행되어야 함
currentActivity.Call("runOnUiThread", new AndroidJavaRunnable(() =>
{
AndroidJavaClass toastClass = new AndroidJavaClass("android.widget.Toast");
AndroidJavaObject context = currentActivity.Call<AndroidJavaObject>("getApplicationContext");
// Toast.makeText(context, message, duration).show(); 호출
AndroidJavaObject toastObject = toastClass.CallStatic<AndroidJavaObject>("makeText", context, message, toastClass.GetStatic<int>("LENGTH_SHORT"));
toastObject.Call("show");
}));
#else
// 에디터에서는 Debug.Log로 대체
Debug.Log($"Toast (Editor): {message}");
#endif
}
// UI 버튼 등에 연결하여 테스트 가능
public void OnButtonClick()
{
ShowToastMessage("Hello from Unity!");
}
}
예시 3: 직접 만든 네이티브 플러그인 메서드 호출 (가정)
// 가정: com.mycompany.myplugin.Calculator 라는 Java 클래스에
// public static int add(int a, int b) 메서드가 존재한다고 가정
using UnityEngine;
public class PluginCaller : MonoBehaviour
{
public int AddUsingPlugin(int num1, int num2)
{
#if UNITY_ANDROID && !UNITY_EDITOR
try
{
AndroidJavaClass pluginClass = new AndroidJavaClass("com.mycompany.myplugin.Calculator");
// 정적 메서드 "add" 호출
int result = pluginClass.CallStatic<int>("add", num1, num2);
Debug.Log($"Plugin calculated: {num1} + {num2} = {result}");
return result;
}
catch (System.Exception e)
{
Debug.LogError("Error calling plugin 'add' method: " + e.Message);
return 0; // 오류 시 기본값 반환
}
#else
Debug.Log("Plugin not available in Editor. Performing calculation in C#.");
return num1 + num2; // 에디터 대체 로직
#endif
}
void Start()
{
AddUsingPlugin(5, 7);
}
}
8. 주의사항 및 모범 사례
- 플랫폼 확인 필수: 안드로이드 전용 코드는 반드시
#if UNITY_ANDROID && !UNITY_EDITOR ... #endif
전처리기 지시문으로 감싸서 다른 플랫폼이나 에디터에서 컴파일 오류가 발생하지 않도록 해야 합니다. 에디터 테스트를 위해 대체 로직을#else
블록에 넣는 것이 좋습니다. - 오류 처리: 네이티브 호출은 다양한 이유(클래스/메서드 이름 오타, 시그니처 불일치, 네이티브 코드 내 예외 발생 등)로 실패할 수 있습니다.
try-catch
블록을 사용하여 예외를 적절히 처리하고, 객체를 반환하는 호출의 경우 반환 값이null
인지 확인하는 것이 안전합니다. - 성능 고려: JNI 호출 오버헤드를 인지하고,
Update
등 자주 호출되는 메서드 내에서의 빈번한 네이티브 호출은 최소화합니다. 필요하다면 네이티브 플러그인 내에서 작업을 묶어 호출 횟수를 줄이도록 설계합니다. - 안드로이드 컨텍스트(Context): 많은 안드로이드 API는
Context
객체(주로Activity
또는Application
컨텍스트)를 요구합니다.UnityPlayer.currentActivity
를 통해 현재Activity
컨텍스트를 얻는 방법을 알아두어야 합니다. - UI 스레드 처리: 안드로이드 UI를 조작하는 모든 작업은 반드시 안드로이드의 메인 UI 스레드에서 수행되어야 합니다. Unity 스레드에서 직접 UI 관련 네이티브 코드를 호출해야 한다면
Activity.runOnUiThread()
를 사용해야 합니다. - 네이티브 플러그인 개발: 간단한 호출 이상으로 복잡한 기능 연동이나 빈번한 상호작용이 필요하다면, 전용 안드로이드 라이브러리 프로젝트(AAR 파일 형태의 플러그인)를 만드는 것이 표준적이고 권장되는 방식입니다. 플러그인 내에서 Java/Kotlin 코드를 깔끔하게 작성하고 C#에서는 이 플러그인의 간소화된 인터페이스와 상호작용하도록 설계하는 것이 좋습니다.
AndroidJavaClass
/Object
는 이때 C#과 플러그인 간의 인터페이스 역할을 합니다. - 메모리 관리:
AndroidJavaObject
/Class
를 통해 얻은 Java 객체 참조를 C#에서 계속 들고 있는 경우, 양쪽 언어의 GC가 상호 작용하는 방식에 유의해야 할 수 있습니다. 대부분의 경우 자동으로 잘 처리되지만, 매우 복잡하거나 장시간 참조를 유지하는 경우 JNI 참조 누수를 방지하기 위해Dispose()
메서드를 명시적으로 호출해야 할 수도 있습니다 (흔한 경우는 아님).
결론
AndroidJavaClass
와 AndroidJavaObject
는 Unity C# 스크립트가 안드로이드 플랫폼의 네이티브 Java/Kotlin 코드와 상호작용할 수 있게 해주는 강력한 도구입니다. 이를 통해 플랫폼 고유 기능 활용, 서드파티 SDK 연동 등 Unity만으로는 구현하기 어려운 기능들을 구현할 수 있습니다. AndroidJavaClass
는 Java 클래스 자체(주로 정적 멤버 접근 및 객체 생성용)를, AndroidJavaObject
는 Java 객체 인스턴스(인스턴스 멤버 접근용)를 나타냅니다. 이들을 사용할 때는 플랫폼 확인 전처리기, 철저한 오류 처리, JNI 호출 성능 오버헤드 인지, 안드로이드 UI 스레드 규칙 준수, 그리고 데이터 타입 변환 방식에 대한 이해가 필수적입니다. 간단한 네이티브 호출은 이 클래스들을 직접 사용하여 구현할 수 있지만, 복잡한 연동을 위해서는 전용 안드로이드 플러그인(AAR)을 개발하고 C#에서는 해당 플러그인과 상호작용하는 것이 더욱 체계적이고 권장되는 접근 방식입니다. 안드로이드 플랫폼을 타겟으로 하는 Unity 개발자에게 이 도구들의 사용법을 익히는 것은 플랫폼의 잠재력을 최대한 활용하기 위한 중요한 기술입니다.