NDK(네이티브 개발 키트)는 Android용 애플리케이션의 일부를 C와 C++로 작성하기 위한 도구 키트입니다. 일반 Android SDK와 달리 NDK는 코드를 네이티브 라이브러리(.so)로 컴파일하여 가상 머신 계층 없이 프로세서에서 직접 작동합니다. Google NDK 가이드, 2026에 따르면 NDK는 고성능 컴퓨팅, 게임, 오디오 처리 및 기존 C/C++ 프로젝트 재사용에 사용됩니다. JNI(Java 네이티브 인터페이스)는 네이티브 코드를 Kotlin 및 Java와 연결합니다.
핵심 요점
NDK(네이티브 개발 키트)는 C와 C++ 코드를 Android용 실행 가능 라이브러리로 크로스 컴파일하기 위한 도구 모음입니다. NDK에는 Clang 컴파일러, libc++ 표준 라이브러리, Android API 헤더 파일 및 빌드 유틸리티가 포함됩니다. 개발자는 C 또는 C++로 네이티브 코드를 작성하고 .so 파일(공유 객체)로 컴파일한 후 JNI를 통해 애플리케이션에 연결합니다.
NDK는 Android SDK를 대체하지 않고 보완합니다. 모든 UI, 라이프사이클 및 시스템 서비스는 Kotlin 또는 Java로 유지됩니다. 네이티브 코드는 특정 작업(수학 계산, 암호화, 코덱, 게임 물리)을 해결합니다. Google은 SDK가 필요한 성능이나 하드웨어 기능에 대한 액세스를 제공하지 않을 때만 NDK 사용을 권장합니다.
NDK의 역사는 2009년(NDK r1)으로 거슬러 올라갑니다. 그동안 도구 키트는 실험적인 스크립트 모음에서 CMake, LLDB 디버깅 및 프로파일링을 지원하는 성숙한 시스템으로 발전했습니다. 2026년 현재 최신 버전은 NDK r27이며 Clang 19, C++20 완전 지원, libc++를 유일한 표준 라이브러리로 제공합니다.
NDK는 네이티브 API를 통해 Android의 저수준 기능에 대한 액세스를 개발자에게 제공합니다. 주요 사용 사례는 다음과 같습니다: 3D 그래픽을 위한 OpenGL ES 및 Vulkan 작업, Oboe를 통한 오디오 처리, BoringSSL을 통한 암호화 작업, ARM용 NEON 최적화. 이러한 모든 API는 C/C++에서 사용 가능하며 컴파일을 위해 NDK가 필요합니다.
또한 NDK를 사용하면 기존 C/C++ 라이브러리를 Kotlin으로 다시 작성하지 않고 재사용할 수 있습니다. 이는 C++에서 긴 역사를 가진 프로젝트(게임 엔진(Unity, Unreal Engine), 컴퓨터 비전 라이브러리(OpenCV), 암호화 패키지(OpenSSL))에 특히 중요합니다. 이러한 경우 NDK는 수년간의 개발 시간을 절약합니다.
| 구성 요소 | 목적 |
|---|---|
| Clang | C++20 지원 C/C++ 컴파일러 |
| libc++ | 표준 C++ 라이브러리(NDK r27에서 유일) |
| CMake | 네이티브 라이브러리 빌드 시스템 |
| LLDB | Android Studio의 네이티브 코드 디버거 |
| ndk-build | 레거시 빌드 시스템(CMake로 대체) |
NDK와 순수 SDK 간의 선택은 작업에 따라 다릅니다. Android SDK는 90%의 시나리오(네트워킹, 파일, 알림, 카메라)에 대해 준비된 Java/Kotlin API를 제공합니다. NDK는 이러한 API가 필요한 성능이나 기능을 제공하지 않을 때 사용됩니다. 예를 들어 SDK를 통한 실시간 오디오 처리는 가능하지만 50~200ms의 지연 시간이 발생하는 반면, Oboe를 통한 네이티브 라이브러리는 지연 시간을 5~15ms로 줄입니다.
APK 크기도 또 다른 요인입니다. NDK를 추가하면 각 ABI 아키텍처에 대해 별도의 .so 라이브러리가 필요하므로 애플리케이션 크기가 증가합니다. 그러나 Android App Bundle(AAB)을 사용하면 문제가 완화됩니다: Google Play는 사용자의 아키텍처에 맞는 라이브러리만 제공합니다. 일반적인 네이티브 프로젝트는 장치당 설치 크기에 1~10MB를 추가합니다.
| 기준 | SDK(Kotlin/Java) | NDK(C/C++) |
|---|---|---|
| 성능 | 중간(JIT/AOT 컴파일) | 높음(기계어) |
| 오디오 지연 시간 | 50~200ms | Oboe 통해 5~15ms |
| 3D 그래픽 | Canvas/OpenGL Kotlin API 통해 | Vulkan/OpenGL 직접 |
| 개발 복잡성 | 낮음 | 높음(메모리 관리, JNI) |
| 코드 이식성 | Android만 | Linux, Windows, macOS, iOS |
| APK 크기 | 최소 | +1~10MB/라이브러리 |
NDK는 SDK Manager를 통해 Android SDK와 별도로 설치됩니다. NDK 디렉터리 내에는 툴체인(컴파일러), 플랫폼 라이브러리, 헤더 파일 및 유틸리티가 있습니다. NDK 툴체인에는 모든 대상 ABI(arm64-v8a, armeabi-v7a, x86_64, x86)에 대한 크로스 컴파일용 Clang이 포함됩니다. 컴파일러는 CMake 또는 ndk-build 플래그에 따라 필요한 아키텍처를 자동으로 선택합니다.
네이티브 코드용 Android API는 sysroot/usr/include 디렉터리의 헤더 파일로 제공됩니다. 여기에는 네이티브 코드에서 사용 가능한 모든 API의 선언이 있습니다: Activity 라이프사이클용 native_activity.h, 입력 이벤트용 input.h, 센서용 sensor.h. 이러한 헤더를 포함하면 JNI 계층 없이 하드웨어 기능에 액세스할 수 있습니다.
ndk/
toolchains/
llvm/prebuilt/windows-x86_64/
bin/ // Clang, ld, llvm-profdata
sysroot/ // Android API 헤더 파일
platforms/
android-26/ // API 26용 libc, libm, libdl
android-34/
sources/
cxx-stl/ // libc++ 헤더
build/
cmake/ // CMake 툴체인 파일
NDK를 통해 다음 네이티브 API 그룹을 사용할 수 있습니다: Native App Glue(C에서 Activity 라이프사이클 관리), OpenGL ES 3.2 및 Vulkan 1.3(그래픽), Oboe(저지연 오디오), Neural Networks API(온디바이스 머신 러닝). 각 API에는 NDK의 일부로 헤더 파일과 정적/동적 라이브러리가 있습니다.
네이티브 API 작업에는 JNI가 필요하지 않습니다. 함수가 C/C++ 코드에서 직접 호출됩니다. 그러나 Kotlin UI와의 상호 작용에는 여전히 JNI가 필요합니다. 이 하이브리드 아키텍처는 게임 엔진에서 일반적입니다: C++의 그래픽과 물리(Vulkan 통해), Kotlin의 메뉴와 UI(Jetpack Compose 통해).
JNI(Java 네이티브 인터페이스)는 Java/Kotlin 가상 머신에서 네이티브 코드를 호출하는 표준 메커니즘입니다. 개발자는 Kotlin에서 external 키워드로 외부 함수를 선언하고 System.loadLibrary를 통해 .so 라이브러리를 로드합니다. C++ 측에서는 함수가 JNI 명명 규칙을 사용하여 선언되며, 패키지와 클래스 이름이 인코딩됩니다.
class NativeBridge {
companion object {
init {
System.loadLibrary("native-lib")
}
}
external fun stringFromJNI(): String
external fun fibonacci(n: Int): Long
external fun processBuffer(data: ByteArray): ByteArray
}
// 코드에서 사용
val bridge = NativeBridge()
println(bridge.stringFromJNI()) // "Hello from C++"
#include <jni.h>
#include <string>
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_app_NativeBridge_stringFromJNI(
JNIEnv* env, jobject /* this */) {
std::string hello = "Hello from C++ NDK";
return env->NewStringUTF(hello.c_str());
}
extern "C" JNIEXPORT jlong JNICALL
Java_com_example_app_NativeBridge_fibonacci(
JNIEnv* env, jobject /* this */, jint n) {
if (n <= 1) return n;
jlong a = 0, b = 1;
for (int i = 2; i <= n; i++) {
jlong temp = a + b;
a = b;
b = temp;
}
return b;
}
CMake는 NDK의 기본 빌드 시스템으로, 기존 ndk-build를 대체했습니다. CMakeLists.txt 파일은 소스 파일, 라이브러리 및 컴파일 플래그를 설명합니다. Android Studio는 build.gradle에 externalNativeBuild가 구성된 경우 프로젝트 빌드 시 자동으로 CMake를 호출합니다. CMake는 각 대상 ABI에 대한 메이크파일을 생성하고 네이티브 코드를 병렬로 컴파일합니다.
cmake_minimum_required(VERSION 3.22.1)
project("nativelib")
# 헤더 파일 포함
include_directories(src/main/cpp/include)
# 네이티브 라이브러리 생성
add_library(
native-lib
SHARED
src/main/cpp/native-lib.cpp
src/main/cpp/math_utils.cpp
src/main/cpp/audio_processor.cpp
)
# Android 시스템 라이브러리 링크
target_link_libraries(
native-lib
android
log
OpenSLES
# libc++ 자동 링크
)
# 최적화 플래그
target_compile_options(native-lib PRIVATE -O3 -Wall -Wextra)
android {
defaultConfig {
ndk {
// 빌드 대상 ABI
abiFilters "arm64-v8a", "armeabi-v7a", "x86_64"
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
version "3.22.1"
}
}
buildTypes {
release {
externalNativeBuild {
cmake {
arguments "-DCMAKE_BUILD_TYPE=Release"
}
}
}
}
}
ABI(애플리케이션 바이너리 인터페이스)는 장치 프로세서와의 호환성을 결정하는 기계어 형식입니다. 각 ARM 또는 x86 아키텍처에는 고유한 ABI가 있습니다. NDK는 지정된 각 ABI에 대해 네이티브 코드를 별도로 컴파일합니다. 2026년 기준 가장 일반적인 ABI: arm64-v8a(최신 기기의 99%), armeabi-v7a(구형 32비트 기기), x86_64(에뮬레이터 및 Chromebook).
build.gradle에서 abiFilters를 지정하면 필요한 아키텍처로만 빌드가 제한되어 컴파일 시간이 단축됩니다. Google Play의 경우 라이브러리가 지원하는 모든 ABI를 포함하는 것이 좋습니다. 그러면 모든 기기에서 작동이 보장됩니다. Google Play Console에서는 App Bundle을 통해 ABI별 APK 전달을 구성할 수 있습니다.
#include <jni.h>
#include <android/api-level.h>
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_app_NativeBridge_getABIInfo(
JNIEnv* env, jobject /* this */) {
#if defined(__arm__)
#if defined(__ARM_ARCH_7A__)
return env->NewStringUTF("armeabi-v7a");
#endif
#elif defined(__aarch64__)
return env->NewStringUTF("arm64-v8a");
#elif defined(__x86_64__)
return env->NewStringUTF("x86_64");
#elif defined(__i386__)
return env->NewStringUTF("x86");
#endif
return env->NewStringUTF("unknown");
}
| ABI | 아키텍처 | 비트 | 장치 |
|---|---|---|---|
| arm64-v8a | ARMv8-A | 64비트 | 거의 모든 최신 휴대폰 |
| armeabi-v7a | ARMv7-A | 32비트 | 구형 장치(2020년 이전) |
| x86_64 | x86-64 | 64비트 | 에뮬레이터, Chromebook |
| x86 | x86 IA-32 | 32비트 | 레거시 에뮬레이터 |
실용적인 예를 살펴보겠습니다: 부동 소수점 숫자 작업을 위한 수학 라이브러리입니다. C++의 네이티브 코드는 ARM NEON 명령어에 직접 액세스하고 런타임에 배열 경계 검사가 없기 때문에 Kotlin보다 효율적으로 계산을 수행합니다. 이 예제는 NDK의 일반적인 사용 패턴인 무거운 계산을 네이티브 계층으로 오프로드하는 방법을 보여줍니다.
#include <jni.h>
#include <cmath>
#include <vector>
extern "C" JNIEXPORT jfloatArray JNICALL
Java_com_example_app_NativeBridge_normalizeArray(
JNIEnv* env, jobject, jfloatArray input) {
jsize len = env->GetArrayLength(input);
jfloat* elements = env->GetFloatArrayElements(input, nullptr);
// 평균 및 표준 편차 계산
float sum = 0.0f, sumSq = 0.0f;
for (jsize i = 0; i < len; i++) {
sum += elements[i];
sumSq += elements[i] * elements[i];
}
float mean = sum / len;
float stddev = std::sqrt(sumSq / len - mean * mean);
// 정규화: (x - mean) / stddev
jfloat* result = new jfloat[len];
for (jsize i = 0; i < len; i++) {
result[i] = (elements[i] - mean) / stddev;
}
env->ReleaseFloatArrayElements(input, elements, JNI_ABORT);
jfloatArray output = env->NewFloatArray(len);
env->SetFloatArrayRegion(output, 0, len, result);
delete[] result;
return output;
}
네이티브 코드를 디버깅하려면 android/log.h 라이브러리의 __android_log_print 매크로를 사용하세요. 메시지는 Java/Kotlin 로그와 함께 Logcat에 나타납니다. 로깅 수준(ANDROID_LOG_DEBUG, ANDROID_LOG_ERROR)으로 메시지를 필터링할 수 있습니다.
#include <android/log.h>
#define LOG_TAG "NativeLib"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
extern "C" JNIEXPORT void JNICALL
Java_com_example_app_NativeBridge_processData(
JNIEnv* env, jobject, jint count) {
LOGD("Processing %d items", count);
for (int i = 0; i < count; i++) {
// 무거운 처리
LOGD("Item %d processed", i);
}
LOGD("Processing complete");
}
자주 묻는 질문
아니요. 대부분의 애플리케이션에는 Kotlin SDK로 충분합니다. NDK가 필요한 경우는 실시간 오디오/비디오 처리, 3D 그래픽, 암호화 또는 기존 C/C++ 프로젝트 재사용과 같은 고성능 작업입니다.
NDK는 LLVM 툴체인의 Clang을 사용합니다. NDK r23부터 GCC는 완전히 제거되었습니다. Clang은 모든 Android ABI(arm64-v8a, armeabi-v7a, x86_64, x86)에 대해 크로스 컴파일합니다.
ABI(애플리케이션 바이너리 인터페이스)는 프로세서와의 호환성을 결정하는 기계어 형식입니다. NDK는 각 ABI에 대해 별도로 .so 라이브러리를 빌드합니다. arm64-v8a는 최신 Android 기기의 기본 ABI입니다.
네. Android Studio는 네이티브 코드 디버거인 LLDB를 지원합니다. C++ 파일에 중단점을 설정하고 변수 및 호출 스택을 볼 수 있습니다. NDK와 LLDB 플러그인이 필요합니다.
각 네이티브 라이브러리는 APK에 100KB에서 수 MB를 추가합니다. 각 ABI에 대해 별도의 .so 라이브러리가 필요합니다. Android App Bundle은 사용자에게 적합한 아키텍처만 제공하여 설치 크기를 줄입니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.