NDK Android: 네이티브 개발 키트와 C++를 위한 JNI의 이해

저자: IT Sectr 게시일: 2026-02-09 읽는 시간: 10 분

NDK(네이티브 개발 키트)는 Android용 애플리케이션의 일부를 C와 C++로 작성하기 위한 도구 키트입니다. 일반 Android SDK와 달리 NDK는 코드를 네이티브 라이브러리(.so)로 컴파일하여 가상 머신 계층 없이 프로세서에서 직접 작동합니다. Google NDK 가이드, 2026에 따르면 NDK는 고성능 컴퓨팅, 게임, 오디오 처리 및 기존 C/C++ 프로젝트 재사용에 사용됩니다. JNI(Java 네이티브 인터페이스)는 네이티브 코드를 Kotlin 및 Java와 연결합니다.

핵심 요점

  • NDK — C/C++ 코드를 Android용 네이티브 라이브러리로 컴파일하는 도구 키트입니다.
  • JNI — Java/Kotlin 가상 머신과 네이티브 코드 간의 상호 작용 인터페이스입니다.
  • CMake — NDK 프로젝트의 기본 빌드 시스템, CMakeLists.txt를 통해 구성 가능합니다.
  • ABI — 대상 프로세서 아키텍처(arm64-v8a, armeabi-v7a, x86_64)입니다.
  • NDK는 대부분의 Android 앱에 필요하지 않으며 높은 성능이 요구되는 작업에만 사용됩니다.

NDK란

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의 주요 기능

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는 수년간의 개발 시간을 절약합니다.

구성 요소목적
ClangC++20 지원 C/C++ 컴파일러
libc++표준 C++ 라이브러리(NDK r27에서 유일)
CMake네이티브 라이브러리 빌드 시스템
LLDBAndroid Studio의 네이티브 코드 디버거
ndk-build레거시 빌드 시스템(CMake로 대체)

NDK vs SDK: 네이티브 코드가 필요한 경우

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와 NDK 비교

기준SDK(Kotlin/Java)NDK(C/C++)
성능중간(JIT/AOT 컴파일)높음(기계어)
오디오 지연 시간50~200msOboe 통해 5~15ms
3D 그래픽Canvas/OpenGL Kotlin API 통해Vulkan/OpenGL 직접
개발 복잡성낮음높음(메모리 관리, JNI)
코드 이식성Android만Linux, Windows, macOS, iOS
APK 크기최소+1~10MB/라이브러리

NDK 아키텍처: 도구와 라이브러리

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 디렉터리 구조

text
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 툴체인 파일

네이티브 Android API

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: Kotlin이 C++를 호출하는 방법

JNI(Java 네이티브 인터페이스)는 Java/Kotlin 가상 머신에서 네이티브 코드를 호출하는 표준 메커니즘입니다. 개발자는 Kotlin에서 external 키워드로 외부 함수를 선언하고 System.loadLibrary를 통해 .so 라이브러리를 로드합니다. C++ 측에서는 함수가 JNI 명명 규칙을 사용하여 선언되며, 패키지와 클래스 이름이 인코딩됩니다.

Kotlin에서 네이티브 함수 선언

kotlin
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++"

C++에서 JNI 함수 구현

cpp
#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;
}

NDK용 CMakeLists.txt 설정

CMake는 NDK의 기본 빌드 시스템으로, 기존 ndk-build를 대체했습니다. CMakeLists.txt 파일은 소스 파일, 라이브러리 및 컴파일 플래그를 설명합니다. Android Studio는 build.gradle에 externalNativeBuild가 구성된 경우 프로젝트 빌드 시 자동으로 CMake를 호출합니다. CMake는 각 대상 ABI에 대한 메이크파일을 생성하고 네이티브 코드를 병렬로 컴파일합니다.

네이티브 라이브러리용 CMakeLists.txt 예제

cmake
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)

NDK용 build.gradle 구성

groovy
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 및 멀티플랫폼 빌드

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 전달을 구성할 수 있습니다.

네이티브 코드에서 장치 ABI 확인

cpp
#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-v8aARMv8-A64비트거의 모든 최신 휴대폰
armeabi-v7aARMv7-A32비트구형 장치(2020년 이전)
x86_64x86-6464비트에뮬레이터, Chromebook
x86x86 IA-3232비트레거시 에뮬레이터

C++ 네이티브 코드 예제

실용적인 예를 살펴보겠습니다: 부동 소수점 숫자 작업을 위한 수학 라이브러리입니다. C++의 네이티브 코드는 ARM NEON 명령어에 직접 액세스하고 런타임에 배열 경계 검사가 없기 때문에 Kotlin보다 효율적으로 계산을 수행합니다. 이 예제는 NDK의 일반적인 사용 패턴인 무거운 계산을 네이티브 계층으로 오프로드하는 방법을 보여줍니다.

네이티브 코드의 수학 연산

cpp
#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)으로 메시지를 필터링할 수 있습니다.

cpp
#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");
}

자주 묻는 질문

Android 개발에 NDK 사용이 필수인가요?

아니요. 대부분의 애플리케이션에는 Kotlin SDK로 충분합니다. NDK가 필요한 경우는 실시간 오디오/비디오 처리, 3D 그래픽, 암호화 또는 기존 C/C++ 프로젝트 재사용과 같은 고성능 작업입니다.

NDK는 어떤 컴파일러를 사용하나요?

NDK는 LLVM 툴체인의 Clang을 사용합니다. NDK r23부터 GCC는 완전히 제거되었습니다. Clang은 모든 Android ABI(arm64-v8a, armeabi-v7a, x86_64, x86)에 대해 크로스 컴파일합니다.

NDK 컨텍스트에서 ABI란 무엇인가요?

ABI(애플리케이션 바이너리 인터페이스)는 프로세서와의 호환성을 결정하는 기계어 형식입니다. NDK는 각 ABI에 대해 별도로 .so 라이브러리를 빌드합니다. arm64-v8a는 최신 Android 기기의 기본 ABI입니다.

NDK를 통해 C++ 코드를 디버깅할 수 있나요?

네. Android Studio는 네이티브 코드 디버거인 LLDB를 지원합니다. C++ 파일에 중단점을 설정하고 변수 및 호출 스택을 볼 수 있습니다. NDK와 LLDB 플러그인이 필요합니다.

NDK가 APK 크기에 어떤 영향을 미치나요?

각 네이티브 라이브러리는 APK에 100KB에서 수 MB를 추가합니다. 각 ABI에 대해 별도의 .so 라이브러리가 필요합니다. Android App Bundle은 사용자에게 적합한 아키텍처만 제공하여 설치 크기를 줄입니다.

요약

  • NDK — Clang 컴파일러를 사용하여 C/C++ 코드를 Android용 네이티브 라이브러리로 크로스 컴파일하는 도구 모음입니다.
  • JNI — external 키워드와 함수 명명 규칙을 통해 Kotlin/Java를 네이티브 코드와 연결하는 인터페이스입니다.
  • CMake — NDK의 기본 빌드 시스템, CMakeLists.txt와 Gradle의 externalNativeBuild로 구성합니다.
  • ABI는 프로세서 아키텍처(arm64-v8a, armeabi-v7a, x86_64)를 정의합니다. 각 ABI에 별도의 라이브러리 빌드가 필요합니다.
  • NDK는 높은 성능이 필요한 작업(게임, 오디오, 그래픽, 암호화, 온디바이스 머신 러닝)에만 사용하세요.
  • Google Play는 App Bundle을 통한 ABI 분할을 지원합니다: 사용자는 자신의 기기 아키텍처용 라이브러리만 받습니다.
  • 네이티브 라이브러리는 APK 크기를 크게 증가시키지만 중요한 작업에 최대 성능과 낮은 지연 시간을 제공합니다.

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기