하나의 C++ 코어로 iOS와 Android를 — 네이티브 코드, 크로스 컴파일, .so/.a, JNI, Objective-C++
공통 C++ 코어를 Android와 iOS에 배포할 때 필요한 ABI별 빌드와 플랫폼 바인딩을 설명합니다. Android의 JNI와 shared library, iOS의 Objective-C++와 static library·XCFramework를 구분하고, 네이티브 코어 업데이트가 앱 재빌드와 배포를 요구하는 이유도 함께 다룹니다.
목차(28개 항목)
- 0. libwebrtc 기반 SDK — 소스는 공유하고 산출물은 플랫폼별로 만든다
1. libwebrtc가 C++를 쓰는 이유 — 성능과 멀티플랫폼 자산
2. 네이티브 코드 · 컴파일 · 크로스 컴파일 · ARM ABI
3. 정적·동적 링킹과 OS별 파일 형식
4. Android의 연결 — JNI로 Java/Kotlin ↔ C++ 잇기
5. iOS의 연결 — Objective-C++(.mm)와 Swift-C++ Interop
- 6. 래퍼(Wrapper) — 앱 개발자가 실제로 만지는 것
7. 업데이트 = 앱 재빌드 + 스토어 재배포
- 8. 전체 그림 한 장 — 하나의 C++ 코어가 두 OS로
- 9. 암기 카드 — 한 줄 요약
- 10. 한 줄 결론
- 관련 글
- 참고 자료
"libwebrtc가 C++로 짜여 있다는데, iOS와 Android에서 그 코드를 어떻게 같이 쓰나요?" 같은 C++ 소스를 각 플랫폼용으로 빌드하고 플랫폼별 바인딩을 붙이는 것이 일반적인 구조입니다. 다만 모든 WebRTC 제품이 하나의 공통 코어를 쓰는 것은 아닙니다.
이 글은 그 구조를 처음부터 끝까지 정리합니다. 왜 코어가 C++인지, 네이티브 코드와 크로스 컴파일이 정확히 뭔지, .a와 .so의 진짜 구분 기준은 무엇인지(여기서 흔한 오해를 정정합니다), Android는 JNI로·iOS는 Objective-C++로 어떻게 C++를 부르는지를 차례로 짚습니다. 마지막으로 이 구조의 트레이드오프(업데이트하려면 앱을 다시 빌드해야 한다)까지 다룹니다.
0. libwebrtc 기반 SDK — 소스는 공유하고 산출물은 플랫폼별로 만든다
libwebrtc 기반 모바일 SDK는 공통 C++ 소스를 각 target OS·architecture·ABI에 맞춰 빌드한다. Android는 주로 JNI로 Kotlin/Java와 연결하고, Apple 플랫폼은 Objective-C++ 또는 Swift-C++ interoperability를 사용할 수 있다. 바인딩은 타입·수명·오류 경계를 관리해야 하므로 항상 얇거나 자동인 것은 아니다.
흔한 오해와 정정부터 박아두겠습니다. 이 표 하나가 이 글 전체의 요약입니다.
| 오해 | 정확한 이해 |
|---|---|
| iOS와 Android는 RTC 코드를 항상 각자 다시 작성한다 | libwebrtc 기반 SDK는 공통 C++ 소스를 재사용할 수 있지만 플랫폼 통합 코드는 별도 |
.so는 동적, .a는 정적이라는 개념만 알면 된다 | 정적/동적 링킹 개념은 공통이지만 실제 파일 형식은 OS별로 다름. Android는 ELF .so, Apple 플랫폼은 Mach-O dylib/framework를 사용 |
ARM 칩이면 다 같으니 .so 하나로 모든 안드로이드폰에서 돈다 | ❌ ABI(armeabi-v7a / arm64-v8a / x86_64…)가 다르면 호환 안 됨. APK에 여러 .so를 담음 |
| C++ 코어는 OS가 알아서 자동으로 업데이트해준다 | ❌ 코어는 앱 바이너리에 박혀 배포됨. 업데이트 = 앱 재빌드 + 스토어 재배포 |
| iOS도 JNI가 필요하다 | JNI는 필요 없지만 ObjC++/Swift-C++에도 타입·소유권·ABI 경계가 존재 |
1. libwebrtc가 C++를 쓰는 이유 — 성능과 멀티플랫폼 자산
실시간 미디어 엔진은 짧은 처리 deadline과 일정한 지연이 중요합니다. C++는 네이티브 코드, SIMD, 메모리 배치와 기존 미디어 생태계를 함께 활용할 수 있어 libwebrtc의 핵심 구현 언어로 사용됩니다. 이 요구가 C++만을 강제하는 것은 아니며 Rust 같은 대안도 가능합니다.
C++를 고르는 세 가지 이유
1. 네이티브 성능과 저수준 제어 C++는 가상 머신이나 인터프리터를 거치지 않고 CPU 기계어로 직접 컴파일됩니다. 코덱의 DSP 루프, SIMD 벡터 연산(NEON/SSE), 메모리 정렬 같은 저수준 최적화를 그대로 표현할 수 있습니다.
2. 런타임 일시정지를 제어하기 쉬움
Swift는 GC가 아니라 ARC를 사용합니다. GC·ARC·수동 메모리 관리는 각각 비용과 안전성 특성이 다르며, GC 없음만으로 언어 선택을 설명할 수 없습니다. libwebrtc에서는 기존 C++ 코드와 플랫폼 이식성, 저수준 제어가 함께 작용합니다.
3. 멀티플랫폼 코드 재사용
C++ 코어 알고리즘을 여러 target에서 재사용할 수 있습니다. 플랫폼 API, 툴체인, 조건부 컴파일과 바인딩은 별도로 관리해야 하며, 언어 비중을 90% 이상 같은 고정 수치로 일반화하지 않습니다.
트레이드오프: C++는 메모리 안전과 객체 수명 관리 부담이 큽니다. 기존 코드·도구·플랫폼 지원을 재사용하는 이점과 함께 평가해야 합니다.
언어 선택 비교
| 언어 | 성능 | 일시정지 위험 | 멀티플랫폼 코어 | RTC 코어로 |
|---|---|---|---|---|
| C++ | 🟢 네이티브 | 🟢 GC 없음 | 🟢 전 플랫폼 | ✅ 주류 |
| Java/Kotlin (ART) | JIT/AOT 혼합 | GC 영향은 할당 패턴·런타임에 따름 | Android 중심 | 플랫폼 계층에 사용 |
| Swift | 네이티브 | ARC | Apple 중심 | Apple 전용 코어에 사용 가능 |
| Rust | 네이티브 | GC 없음 | 여러 target | 신규 네이티브 코어 선택지 |
2. 네이티브 코드 · 컴파일 · 크로스 컴파일 · ARM ABI
코어가 C++라는 건 알았습니다. 그런데 "C++ 소스"가 곧바로 폰에서 도는 건 아닙니다. 사람이 읽는 텍스트일 뿐이고, CPU가 실행하려면 기계어(machine code)로 번역해야 합니다. 이 번역이 컴파일입니다.
네이티브 코드란?
네이티브 코드 = 특정 CPU가 직접 실행할 수 있는 기계어 명령어. 중간 가상 머신(JVM 등)을 거치지 않고 하드웨어에서 바로 도는 코드.
크로스 컴파일이란?
여기서 모바일 개발 특유의 문제가 생깁니다. 개발자는 Mac이나 PC에서 코드를 짜지만, 그 코드는 폰의 ARM CPU에서 돌아야 합니다. Mac의 CPU와 폰의 CPU는 명령어 집합이 다릅니다. 그래서 "내 컴퓨터와 다른 종류의 CPU용 기계어를 만드는" 컴파일이 필요합니다.
크로스 컴파일(Cross-compilation): 컴파일러가 실행되는 플랫폼(Host)과 다른 플랫폼(Target)의 실행 코드를 생성하는 것.
- Host = 컴파일러가 도는 곳 (보통 개발자의 Mac/PC)
- Target = 생성된 코드가 실행될 곳 (폰의 ARM 기기)
Host와 target이 다를 때 cross-compilation이라고 합니다. 같은 ARM64라도 target OS의 ABI와 파일 형식이 다르면 산출물을 서로 바꿔 쓸 수 없습니다. Android는 NDK, iOS는 Xcode 툴체인이 해당 target 구성을 제공합니다.
ARM ISA는 같은데 왜 하나의 바이너리로 안 되나 — ABI 문제
흔한 오해를 정정합니다.
정정: "스냅드래곤·미디어텍·Apple Silicon이 다 ARM이니까 ARM 기계어
.so하나면 모든 안드로이드폰에서 돈다" → FALSE.
ARM ISA(명령어 집합 구조)를 공유하더라도 ABI(Application Binary Interface)가 다르면 호환되지 않습니다. ABI는 명령어뿐 아니라 함수 호출 규약, 레지스터 사용법, 데이터 정렬, 32비트/64비트 여부까지 포함한 "바이너리 수준의 약속"입니다.
Android가 지원하는 주요 ABI:
| ABI | 설명 | 비트 |
|---|---|---|
armeabi-v7a | 32비트 ARM (Thumb-2) | 32-bit |
arm64-v8a | 64비트 ARM (AArch64) — 현재 권장 | 64-bit |
x86 | 32비트 인텔 (에뮬레이터 등) | 32-bit |
x86_64 | 64비트 인텔 (에뮬레이터, 일부 기기) | 64-bit |
하나의 .so 바이너리는 빌드 대상 ABI와 OS 계약에 맞아야 합니다. 64비트 ARM 하드웨어라도 OS가 32비트 userspace를 지원하지 않을 수 있으므로 AArch32 실행을 보장할 수 없습니다. Android App Bundle/APK는 지원할 ABI별 라이브러리를 포함하도록 구성합니다.
따라서 "ARM이면 다 같다"가 아니라, 같은 C++ 소스 → ABI 개수만큼 크로스 컴파일 → 각각 별도 바이너리가 정확한 그림입니다.
3. 정적·동적 링킹과 OS별 파일 형식
이 글에서 가장 자주 틀리는 지점입니다. 명확히 정정하겠습니다.
정정: "
.so는 Android(리눅스)용,.a는 iOS(유닉스)용 기계어 파일이다" → FALSE.
정적·동적 링킹이라는 개념은 OS에 공통이지만 파일 형식과 확장자는 OS별입니다. .a는 정적 archive에 널리 쓰이고 Android/Linux는 ELF .so, Apple 플랫폼은 Mach-O dylib 또는 framework를 사용합니다.
Static vs Dynamic — 진짜 구분 기준
| 항목 | .a (Static Archive) | .so (Shared Object) |
|---|---|---|
| 결합 시점 | 컴파일/링크 시점 | 런타임(실행 중) 로드 |
| 코드 위치 | 앱 바이너리 안에 복사됨 | 별도 파일로 존재, 실행 시 로드 |
| 앱 크기 | 커짐 (코드 포함) | 작아짐 (외부 참조) |
| 공유 | 불가 (각 앱이 복사본) | 여러 프로세스가 공유 가능 |
| 개념의 소속 | OS 무관 — 링킹 방식 | OS 무관 — 링킹 방식 |
비유: .a는 책 내용을 통째로 복사해서 내 보고서에 붙여넣는 것(보고서가 두꺼워지지만 독립적), .so는 참고문헌으로 링크만 걸어두는 것(보고서는 얇지만 그 문헌이 옆에 있어야 함). 기술적 실체 — Static은 링커가 .a 안의 필요한 오브젝트를 앱 실행 파일에 직접 병합하고, Dynamic은 실행 시 동적 링커가 .so를 메모리에 매핑합니다.
두 OS 모두 둘 다 지원한다 — 이게 핵심
Static (.a) | Dynamic | 비고 | |
|---|---|---|---|
| Android | ✅ 지원 (다른 라이브러리에 링크) | ✅ .so (NDK 기본 산출물) | NDK가 C/C++ 소스로 .so를 빌드 |
| iOS | ✅ .a / 정적 framework | ✅ Mach-O dylib / 동적 framework | XCFramework는 여러 플랫폼·architecture 변형을 묶는 배포 컨테이너 |
Android NDK 공식 설명을 그대로 옮기면:
- Native Libraries (.so files) — NDK가 C/C++ 소스에서 빌드하는 동적 라이브러리
- Static libraries (.a files) — 다른 라이브러리에 링크해 넣을 수 있는 정적 라이브러리
Android도 정적 archive를 다른 native library에 링크할 수 있고, Apple 플랫폼도 정적·동적 framework를 지원합니다. XCFramework가 정적 또는 동적을 자동으로 결정하지는 않습니다.
.a/.so는 "정적이냐 동적이냐"이지 "iOS냐 Android냐"가 아닙니다. Static vs Dynamic은 OS 독립적 개념입니다.
그럼 WebRTC는 실제로 어떻게 배포되나
판정: PARTLY_TRUE — "iOS는 WebRTC.framework, Android는 .aar/.so" 는 대체로 맞지만 정확히 정리하면:
WebRTC 코어는 C++로 작성되어 있고 iOS·Android가 같은 C++ 코드베이스를 공유합니다. 다만 배포 형태는 플랫폼별로 다릅니다.
| 플랫폼 | WebRTC 코어를 모바일에 싣는 전형적 형태 | 내용물 |
|---|---|---|
| iOS | .framework 또는 XCFramework | 컴파일된 C++ 코어 + Objective-C 래퍼 |
| Android | .aar (Android Archive) 또는 직접 .so | 컴파일된 C++ 코어(.so) + Java/Kotlin 래퍼 |
공식 source tree는 iOS framework와 Android library를 빌드하는 도구를 제공합니다. 사전 빌드 배포 여부와 패키지 형식은 공급자·버전별 릴리스 문서에서 확인합니다.
WebRTC는 오픈소스라 배포 형태가 다양합니다. Google 소스를 빌드하면 iOS는 XCFramework, Android는 AAR+
.so형태로 산출되며, 이를 각 SDK 제공자가 자체 래퍼와 함께 재배포합니다. 그래서 "정확히 어떤 파일 확장자냐"는 누가 배포했느냐에 따라 달라집니다.
4. Android의 연결 — JNI로 Java/Kotlin ↔ C++ 잇기
Android 앱의 Kotlin/Java 코드는 DEX bytecode로 컴파일되어 ART(Android Runtime) 에서 실행됩니다. 네이티브 C++ 라이브러리와의 호출 경계에 JNI를 사용할 수 있습니다.
JNI란?
JNI(Java Native Interface): Java/Kotlin 코드와 C/C++ 네이티브 코드가 서로 호출할 수 있게 해주는 표준 규격.
JNI는 Java 코드와 native code 사이의 표준 인터페이스입니다. Android에서는 ART와 C/C++ 컴포넌트를 연결할 때 사용합니다.
JNI가 작동하는 3단계
단계별 코드
①② Kotlin: external 선언 + .so 로드
①(external 선언)과 ②(loadLibrary)는 같은 Kotlin 블록 안에서 함께 이뤄집니다.
Java로 쓰면 public native int startCall(String channelId); — 키워드만 native로 다를 뿐 같은 개념입니다.
③ C++: 이름 기반 함수 구현 예
아래는 이름 기반 discovery 예입니다. Android 공식 가이드는 보통 JNI_OnLoad()에서 RegisterNatives()로 명시적으로 등록하는 방식을 권장합니다.
핵심 포인트:
extern "C"— 이름 기반 discovery 예에서 C++ name mangling을 막습니다.RegisterNatives()를 쓰면 이 이름 규칙에 덜 의존합니다.JNIEXPORT/JNICALL— JNI가 인식하는 함수임을 표시하는 매크로.- 타입 변환 — Kotlin의
String은 JVM 객체라 C++의char*로 쓰려면GetStringUTFChars로 변환해야 합니다. JVM 세계와 네이티브 세계는 데이터 표현이 다르기 때문입니다.
JNI 경계에서는 타입 변환, 참조 관리와 예외 처리가 필요할 수 있습니다. primitive와 direct buffer처럼 복사를 피할 수 있는 경로도 있으므로 모든 호출이 데이터를 복사한다고 단정하지 않습니다. 실제 호출 빈도와 payload로 측정합니다.
5. iOS의 연결 — Objective-C++(.mm)와 Swift-C++ Interop
iOS에는 ART/JNI 계층이 없습니다. Swift·Objective-C와 C++는 Objective-C++ 또는 Swift-C++ interoperability로 연결할 수 있지만 타입·소유권·오류 처리 경계는 여전히 존재합니다.
어느 경로가 더 단순한지는 노출할 C++ API와 지원 기능에 따라 달라집니다.
Objective-C++ — 한 파일에서 ObjC와 C++ 혼용
.mm 확장자 파일은 Objective-C++ 컴파일 단위입니다. 같은 파일에서 Objective-C와 C++를 사용할 수 있지만 NSString과 std::string처럼 표현이 다른 타입은 명시적으로 변환하고 수명을 관리해야 합니다.
JNI 코드(섹션 4)와 비교해보면 차이가 분명합니다: extern "C", JNIEXPORT, JNIEnv*, GetStringUTFChars/ReleaseStringUTFChars 같은 의식(ceremony)이 전혀 없습니다. C++ 헤더를 include하고 함수를 바로 호출하면 끝입니다.
Swift에서 C++ 부르기 — 두 갈래
Swift 5.9부터 C++ interoperability를 지원합니다. 다만 C++의 모든 기능을 지원하는 것은 아니며, 지원 범위와 제약은 Swift 공식 status 문서에서 계속 갱신됩니다. 프로젝트가 사용하는 템플릿·소유권·예외 패턴을 실제 툴체인으로 검증해야 합니다.
Android(JNI) vs iOS(ObjC++/Swift) 비교
| 항목 | Android | iOS |
|---|---|---|
| 앱 언어 | Java / Kotlin | Swift / Objective-C |
| 런타임 | ART | Apple native runtime |
| C++ 연결 방식 | JNI (표준 규격 계층) | ObjC++(.mm) / Swift-C++ interop |
| 연결 규약 | JNI, 보통 RegisterNatives() | ObjC++ 또는 Swift-C++ interop 규칙 |
| 데이터 변환 | 타입에 따라 변환·참조 관리 | 타입에 따라 변환·소유권 관리 |
| 표현 | "다리를 놓는다(bridge)" | "같이 컴파일한다" |
Android는 ART와 native code를 JNI로 연결하고, Apple 플랫폼은 ObjC++ 또는 Swift-C++ interop을 사용한다. 두 경로 모두 언어·타입·수명 경계를 설계해야 한다.
6. 래퍼(Wrapper) — 앱 개발자가 실제로 만지는 것
여기까지 보면 의문이 생깁니다. "그럼 RTC 앱 만들 때 매번 JNI나 .mm을 직접 짜야 하나?" 아닙니다. 그 위에 래퍼(Wrapper) 계층이 있습니다.
래퍼: 저수준 C++ 코어를 각 플랫폼의 관용적(idiomatic) API로 감싼 얇은 계층. 앱 개발자는 Kotlin/Swift 객체만 다루고, 내부의 C++ 호출은 래퍼가 숨긴다.
각 계층의 역할:
| 계층 | 역할 | 누가 작성 |
|---|---|---|
| 앱 코드 | 비즈니스 로직, UI | 앱 개발자(우리) |
| 래퍼 | 플랫폼 친화 API 제공 | SDK 제공자(Google/Agora 등) |
| 바인딩 | 언어 경계 연결 (JNI/.mm) | SDK 제공자 |
| C++ 코어 | 실제 미디어 처리 | SDK 제공자 |
핵심: 아래 세 계층(래퍼·바인딩·코어)은 SDK가 이미 만들어 배포합니다. 그래서 앱 개발자는 보통 WebRTC.framework를 import하고 Swift로, 또는 .aar를 의존성에 넣고 Kotlin으로 코어를 호출만 하면 됩니다. JNI·.mm을 직접 짜는 건 코어를 만드는 쪽(SDK 벤더)의 일입니다.
비유: 자동차 엔진(C++ 코어)은 엔지니어가 만들고, 운전자(앱 개발자)는 핸들과 페달(래퍼)만 만집니다. 기술적 실체 — 래퍼는 "Kotlin/Swift로 노출된 공개 API 표면"이고, 그 아래 JNI/.mm이 실제 C++ 함수로 호출을 전달합니다.
7. 업데이트 = 앱 재빌드 + 스토어 재배포
마지막으로 이 구조의 가장 중요한 운영상 트레이드오프입니다.
앱에 포함한 native SDK는 앱 릴리스 경로로 업데이트한다.
C++ 코어(.so/.framework)는 앱 바이너리 안에 포함되어 배포됩니다. 서버에서 동적으로 받아오는 웹 자바스크립트와 근본적으로 다릅니다.
영향 비교
| 항목 | 웹(브라우저 내장) | 네이티브(C++ 코어 내장) |
|---|---|---|
| 코어 업데이트 경로 | 브라우저 공급자와 사용자 업데이트 정책 | 앱 재빌드 + 스토어 재배포 |
| 반영 속도 | 빠름 | 느림 (심사 + 사용자 업데이트 대기) |
| 버전 파편화 | 적음 | 🔴 큼 (구버전 앱 사용자 잔존) |
| 긴급 패치 | 브라우저 rollout과 사용자 버전에 따름 | 스토어 심사·사용자 업데이트에 따름 |
실무 함의: 네이티브 앱은 사용자마다 다른 SDK 버전을 사용할 수 있습니다. 최소 지원 버전, 강제 업데이트 조건과 서버 호환 범위를 미리 설계해야 합니다.
네이티브 SDK는 브라우저보다 넓은 기기 제어권을 얻는 대신 앱 릴리스 주기에 묶입니다.
8. 전체 그림 한 장 — 하나의 C++ 코어가 두 OS로
9. 암기 카드 — 한 줄 요약
| 개념 | 한 줄 요약 |
|---|---|
| 왜 C++ | 네이티브 제어 + 기존 미디어 생태계 + 멀티플랫폼 코드 재사용 |
| 네이티브 코드 | CPU가 직접 실행하는 기계어, VM 안 거침 |
| 크로스 컴파일 | Host(Mac/PC)에서 Target(폰 ARM)용 기계어 생성 |
| ABI | ARM ISA 같아도 ABI 다르면 비호환 → APK에 여러 .so |
| 정적 vs 동적 | 개념은 공통이지만 파일 형식은 OS별로 다름 |
| WebRTC 배포 | iOS=XCFramework/.framework, Android=.aar+.so 형태가 전형적 (실제 확장자는 배포 주체에 따라 다름, 코어는 같은 C++) |
| JNI | ART의 Java/Kotlin↔C++ 경계. RegisterNatives() 또는 이름 기반 discovery 사용 |
| iOS 연결 | ObjC++(.mm)/Swift-C++ interop. 타입·수명 경계는 여전히 관리 |
| 래퍼 | 앱 개발자는 Kotlin/Swift만 만짐, 바인딩·코어는 SDK가 숨김 |
| 업데이트 | 코어가 앱에 박힘 → 재빌드 + 스토어 재배포 필요 |
10. 한 줄 결론
libwebrtc 기반 SDK는 공통 C++ 소스를 target OS·architecture·ABI마다 빌드한다. Android는 JNI, Apple 플랫폼은 ObjC++ 또는 Swift-C++ interop으로 연결한다. 같은 ARM64라도 ABI·파일 형식이 다르면 바이너리를 공유할 수 없고, 앱에 포함한 코어를 올리려면 앱 릴리스가 필요하다.
관련 글
- #43 Agora 자체 코덱 vs Web SDK — SD-RTN, FEC, 레이어 분리 — 네이티브 vs 웹의 코덱/네트워크 레이어 분업
- #0 WebRTC란? ICE? STUN? NAT? TURN? — 이 C++ 코어가 실제로 푸는 문제의 기본기
- #23 오디오 파이프라인 — Opus·RTP·Jitter Buffer — C++ 코어 안에서 도는 실시간 처리 루프
- #28 H.264 Profile·인코더 옵션·비트레이트의 현실 — 코어가 다루는 코덱 레이어 깊게 보기
참고 자료
- Android NDK Concepts —
.so/.a, 네이티브 라이브러리 개념 - Android NDK ABI Management — armeabi-v7a / arm64-v8a / x86_64 등 ABI
- Android JNI Tips —
RegisterNatives, 참조·문자열·호출 경계 권장안 - Oracle JNI Specification — Introduction — JNI 설계 배경과 표준화
- Apple — Objective-C Runtime / Documentation — Objective-C 및 ObjC++ 기반
- Swift C++ Interoperability — Swift 5.9+ C++ 연동
- Swift C++ Interoperability Status — 지원 범위와 제약
- Apple — Dynamic Library Programming Topics — 정적/동적 라이브러리
- GCC Link Options — static/shared 링킹 옵션
- WebRTC Source (Google) — WebRTC C++ 코어 공식 소스