Android System & AOSP Engineering/Native Layer & Daemons

Android NDK 환경 설정부터 C/C++ 네이티브 데몬(Daemon) 예제까지 완벽 가이드

임베디드 친구 2025. 6. 14. 20:01
반응형

안녕하세요! 지난 포스팅에서 안드로이드 앱 성능을 극한으로 끌어올리기 위해 왜 NDK와 C/C++을 써야 하는지, 그리고 백그라운드 데몬(Daemon)의 개념이 무엇인지 가볍게 살펴보았는데요. 개념을 이해했다면 이제 직접 코드를 짜서 돌려볼 차례겠죠?

하지만 네이티브 개발은 시작하기 전 '환경 설정'이라는 거대한 장벽이 버티고 있습니다. NDK 버전 맞추기부터 환경 변수 등록, CMakeLists.txt와 Gradle 스크립트 연동까지... 처음 접하면 셋업 하다가 하루가 다 가버리곤 합니다. 그래서 오늘은 삽질 없이 한 번에 안드로이드 NDK 개발 환경을 구축하고, 5초마다 로그를 찍는 간단한 네이티브 데몬 예제까지 실행하는 전체 과정을 깔끔하게 정리해 드리겠습니다!

Generated by Gemini AI.


📌 핵심 요약 3줄

  • 체계적인 환경 구축: Android Studio SDK Manager와 CLI 방식을 통해 NDK를 올바르게 설치하고 환경 변수를 등록합니다.
  • 빌드 시스템 연동: CMakeLists.txt와 build.gradle 설정을 통해 C/C++ 소스코드가 안드로이드 빌드 파이프라인과 완벽히 통합되도록 구성합니다.
  • 첫 네이티브 데몬 실습: POSIX 스레드(pthread)와 JNI를 활용하여 백그라운드에서 지속 동작하는 간단한 C++ 데몬 프로세스를 직접 구현합니다.

1. Android NDK 설치 및 설정

사용자 정의 데몬을 개발하려면 가장 먼저 C/C++ 코드를 안드로이드 바이너리로 컴파일해 줄 NDK 툴체인을 설치해야 합니다.

1.1 Android NDK 다운로드 및 설치

가장 직관적인 방법은 Android Studio의 UI를 이용하는 것입니다. 다음 순서대로 진행해 주세요.

  1. Android Studio를 실행합니다.
  2. [Tools] > [SDK Manager] 메뉴로 이동합니다.
  3. [SDK Tools] 탭을 클릭합니다.
  4. 리스트에서 "NDK (Side by side)" 옵션을 체크합니다.
  5. "Apply" 버튼을 누르면 다운로드와 설치가 자동으로 진행됩니다.

설치가 완료되면 일반적으로 아래와 같은 경로에 NDK 파일들이 위치하게 됩니다.

  • 기본 경로: $ANDROID_SDK/ndk/<버전_이름>

💡 터미널(CLI)을 선호하신다면?

빌드 서버나 터미널 환경에서 작업 중이라면 아래 명령어를 통해 특정 버전을 바로 설치할 수도 있습니다.

Bash
 
sdkmanager --install "ndk;25.1.8937393"

1.2 운영체제별 환경 변수 설정

터미널에서 NDK 컴파일러에 바로 접근할 수 있도록 환경 변수를 등록해 주어야 합니다. 본인의 OS에 맞는 설정을 적용해 보세요.

📊 OS별 NDK 환경 변수 설정 방법

운영체제 (OS) 설정할 환경 변수 및 명령어
Linux / macOS

(.bashrc 또는 .zshrc)
export ANDROID_NDK_HOME=$ANDROID_SDK/ndk/25.1.8937393

export PATH=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin:$PATH
Windows

(PowerShell 기준)
$env:ANDROID_NDK_HOME="C:\Users\유저명\AppData\Local\Android\Sdk\ndk\25.1.8937393"

$env:Path += ";$env:ANDROID_NDK_HOME\toolchains\llvm\prebuilt\windows-x86_64\bin"

설정을 마쳤다면 터미널을 새로 열고 아래 명령어를 입력해 보세요. Clang 컴파일러 버전 정보가 정상적으로 출력된다면 성공입니다!

Bash
 
clang --version

2. CMake 및 Gradle 설정 방법

파이토치나 일반 C++ 프로젝트처럼 안드로이드에서도 네이티브 코드를 빌드할 때 CMake를 주력으로 사용합니다. Gradle이 이 CMake를 인식하도록 엮어주는 작업이 필요합니다.

2.1 CMakeLists.txt 설정

프로젝트의 루트 또는 src/main/cpp/ 디렉터리에 CMakeLists.txt 파일을 생성하고 아래와 같이 작성합니다. 빌드할 소스코드 파일의 위치와 연결할 시스템 라이브러리(여기서는 로그 라이브러리)를 지정하는 역할을 합니다.

CMake
 
cmake_minimum_required(VERSION 3.10)
project(my_daemon)

# C++17 표준 사용 설정
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 공유 라이브러리(.so) 빌드 정의
add_library(
    my_daemon
    SHARED
    src/main/cpp/daemon.cpp
)

# 안드로이드 시스템 로그 라이브러리 찾기
find_library(
    log-lib
    log
)

# target과 라이브러리 연결
target_link_libraries(
    my_daemon
    ${log-lib}
)

2.2 Gradle 설정 (app/build.gradle)

이제 안드로이드 빌드 시스템인 Gradle에게 "C++ 코드도 같이 빌드해줘!"라고 알려주어야 합니다. app/build.gradle 스크립트의 android 블록 내부에 다음 설정을 추가합니다.

Groovy
 
android {
    compileSdkVersion 33
    ndkVersion "25.1.8937393" # 사용할 NDK 버전 명시

    defaultConfig {
        applicationId "com.example.mydaemon"
        minSdkVersion 21
        targetSdkVersion 33
        versionCode 1
        versionName "1.0"

        # 1. CMake 컴파일러 플래그 설정
        externalNativeBuild {
            cmake {
                cppFlags "-std=c++17 -frtti -fexceptions"
            }
        }
    }

    # 2. CMakeLists.txt 파일 경로 연동
    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
            version "3.10.2"
        }
    }
}

3. Android Studio 및 CLI 빌드 환경 구성

3.1 Android Studio에서 프로젝트 동기화

  1. Android Studio를 실행한 뒤, 설정 파일들이 포함된 프로젝트 디렉터리를 엽니다.
  2. build.gradle이나 CMakeLists.txt를 수정하면 우측 상단에 띄워지는 [Sync Now] 버튼을 클릭해 빌드 시스템을 동기화합니다.

3.2 CLI(터미널)에서 빌드하기

IDE를 켜지 않고 빌드 자동화 스크립트나 터미널에서 곧바로 APK를 빌드하고 싶다면 프로젝트 루트 디렉터리에서 아래 명령어를 실행하면 됩니다.

Bash
 
./gradlew assembleDebug

빌드가 성공적으로 끝나면 app/build/outputs/apk/debug/ 폴더 아래에 결과물인 APK 파일이 생성됩니다.


4. 간단한 Android Daemon 예제

환경 설정이 끝났으니 백그라운드에서 5초마다 "Daemon is running..."이라는 로그를 무한히 찍는 아주 간단한 네이티브 데몬을 만들어 실행해 볼까요?

4.1 C++ Daemon 코드 (src/main/cpp/daemon.cpp)

줄바꿈과 구문을 깔끔하게 정돈한 전체 소스코드입니다. 포인터 반환 구문과 JNI 명명 규칙을 준수하여 작성되었습니다.

C++
 
#include <jni.h>
#include <pthread.h>
#include <android/log.h>
#include <unistd.h>

#define LOG_TAG "MyDaemon"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)

// 백그라운드에서 무한 루프를 돌 스레드 함수
void* daemon_thread(void* arg) {
    while (true) {
        LOGI("Daemon is running...");
        sleep(5); // 5초 대기
    }
    return nullptr;
}

// Java에서 호출할 JNI 네이티브 함수
extern "C" JNIEXPORT void JNICALL
Java_com_example_mydaemon_MainActivity_startDaemon(JNIEnv* env, jobject obj) {
    pthread_t daemonThread;
    // POSIX 스레드를 생성하여 백그라운드로 데몬 루프 실행
    pthread_create(&daemonThread, nullptr, daemon_thread, nullptr);
}

4.2 Java에서 Daemon 호출 (MainActivity.java)

C++로 만든 .so 라이브러리를 로드하고 네이티브 메서드를 선언하여 앱이 켜질 때 실행하는 자바 코드입니다.

Java
 
package com.example.mydaemon;

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {
    
    // 1. 빌드된 네이티브 라이브러리(.so) 로드
    static {
        System.loadLibrary("my_daemon");
    }

    // 2. C++에 선언된 네이티브 함수 연결
    private native void startDaemon();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 3. 앱 시작 시 백그라운드 데몬 스레드 구동
        startDaemon();
    }
}

이제 앱을 빌드하고 실행한 뒤 Android Studio의 Logcat 창에 MyDaemon을 검색해 보세요. 앱 화면에 아무것도 없어도, background에서 5초마다 주기적으로 로그가 올라오는 것을 확인할 수 있습니다!


🛠️ 개발을 위한 꿀팁 (Tips)

  1. 차기 NDK 버전 호환성 체크: 프로젝트에 특별한 레거시 라이브러리가 얽혀있지 않다면 NDK는 가급적 LTS(Long-Term Support) 버전을 선택하는 것이 빌드 안정성 면에서 가장 좋습니다.
  2. local.properties 활용하기: 팀원들과 협업할 때 각자의 PC마다 SDK나 NDK 설치 경로가 다를 수 있습니다. 환경 변수를 글로벌하게 잡는 대신 프로젝트 루트의 local.properties 파일에 ndk.dir=/your/path 형태로 지정해 두면 프로젝트별로 독립된 관리가 가능해집니다.
  3. __android_log_print 매크로 화 활용: 네이티브 코드를 작성할 때 로그 함수 이름이 너무 길어 타이핑하기 번거롭습니다. 예제처럼 LOGI, LOGE, LOGD 등의 매크로를 미리 헤더 파일에 정의해 두면 생산성이 배로 뜁니다.

⚠️ 흔히 하는 실수 (Common Mistakes)

  1. JNI 함수 이름 패키지 매치 오류: 자바에서 UnsatisfiedLinkError가 발생하는 가장 흔한 원인입니다! C++ 측 JNI 함수 이름은 반드시 Java_패키지명_클래스명_함수명 구조를 정확히 따라야 합니다. 대소문자나 언더바(_) 하나만 틀려도 자바가 함수를 찾지 못하니 주의하세요.
  2. CMake에 소스 파일 누락: 새로운 .cpp 파일이나 .h 헤더 파일을 추가해 두고 CMakeLists.txt 파일의 add_library 항목에 한 줄 추가해 주지 않으면 컴파일 과정에서 참조 에러(Undefined reference)가 발생합니다.
  3. 부모 스레드 종료와 데몬의 생명주기: 예제에서 사용한 pthread 방식은 완벽하게 독립된 리눅스 레벨의 데몬이라기보다는 '앱 프로세스 안에서 도는 백그라운드 네이티브 스레드'에 가깝습니다. 따라서 안드로이드 시스템이 앱 프로세스 자체를 강제 종료(Kill)하면 이 스레드도 함께 죽게 됩니다. 진짜 독립된 상주형 데몬을 만들려면 리눅스의 fork() 개념과 시스템 권한(Rooting 혹은 System App 권한)에 대한 추가 처리가 필요합니다.

5. 결론

이번 포스팅에서는 Android NDK를 활용하여 C/C++ 기반 개발을 진행하기 위한 가장 기초적이고 핵심적인 환경 설정 단계를 살펴보았습니다.

단순히 UI를 통한 설치에 그치지 않고, CMake와 Gradle이 유기적으로 맞물려 네이티브 라이브러리를 빌드하는 메커니즘을 이해하는 것이 중요합니다. 더불어 JNI를 통해 자바에서 C++ 스레드를 제어하는 기본 뼈대 코드까지 실습해 보았으니, 이제 더 고도화된 백그라운드 로직을 얹을 준비가 끝나신 셈입니다.

다음에는 이 뼈대 위에 리눅스 저수준 파일 제어나 IPC 통신 같은 본격적인 데몬 기능을 얹는 법을 다루어 보겠습니다. 진행하시다가 셋업 과정에서 막히는 에러 메시지가 있다면 주저 말고 댓글로 남겨주세요! 같이 해결해 봅시다.

반응형