블로그 목록
Fundamentals25분 읽기

하나의 C++ 코어로 iOS와 Android를 — 네이티브 코드, 크로스 컴파일, .so/.a, JNI, Objective-C++

"WebRTC 엔진이 C++라는데, iOS랑 Android에서 그 코드를 같이 쓰는 건가요? 플랫폼마다 다시 짜는 거 아닌가요?" 모바일 RTC 도입 자리에서 자주 나오는 질문입니다. 답은 코어 로직을 C++ 하나로 짜고 플랫폼마다 얇은 연결 계층만 따로 붙인다는 것입니다. 왜 코어가 C++인지, ABI가 다르면 왜 .so 하나로 안 되는지, 정적과 동적 라이브러리의 진짜 구분 기준은 무엇인지, Android는 JNI로 iOS는 Objective-C++로 어떻게 잇는지, 그리고 코어가 앱에 박혀 있어 업데이트하려면 앱을 다시 빌드해야 한다는 대가까지 차근히 짚어 드립니다.

C++JNIObjective-C++크로스 컴파일ABIARM.so.aNDKXCFramework네이티브기초
목차(28개 항목)
  1. 0. 핵심 명제 — 코어는 하나, 연결 계층은 플랫폼마다
  2. 1. 왜 코어가 C++인가 — 성능, GC 없음, 멀티플랫폼
    1. C++를 고르는 세 가지 이유
    2. 언어 선택 비교
  3. 2. 네이티브 코드 · 컴파일 · 크로스 컴파일 · ARM ABI
    1. 네이티브 코드란?
    2. 크로스 컴파일이란?
    3. ARM ISA는 같은데 왜 하나의 바이너리로 안 되나 — ABI 문제
  4. 3. ★중요 정정 — `.a`(static) vs `.so`(shared)는 OS 구분이 아니다
    1. Static vs Dynamic — 진짜 구분 기준
    2. 두 OS 모두 둘 다 지원한다 — 이게 핵심
    3. 그럼 WebRTC는 실제로 어떻게 배포되나
  5. 4. Android의 연결 — JNI로 Java/Kotlin ↔ C++ 잇기
    1. JNI란?
    2. JNI가 작동하는 3단계
    3. 단계별 코드
  6. 5. iOS의 연결 — Objective-C++(.mm)와 Swift-C++ Interop
    1. Objective-C++ — 한 파일에서 ObjC와 C++ 혼용
    2. Swift에서 C++ 부르기 — 두 갈래
    3. Android(JNI) vs iOS(ObjC++/Swift) 비교
  7. 6. 래퍼(Wrapper) — 앱 개발자가 실제로 만지는 것
  8. 7. 업데이트 = 앱 재빌드 + 스토어 재배포
    1. 영향 비교
  9. 8. 전체 그림 한 장 — 하나의 C++ 코어가 두 OS로
  10. 9. 암기 카드 — 한 줄 요약
  11. 10. 한 줄 결론
  12. 관련 글
  13. 참고 자료

"WebRTC 엔진이 C++로 짜여 있다는데, 그럼 iOS랑 Android에서 그 코드를 어떻게 같이 쓰나요? 플랫폼마다 다시 짜는 거 아닌가요?". 모바일 RTC 도입을 검토하는 자리에서 자주 받는 질문입니다. 답부터 말하면 — 코어 로직은 C++ 하나로 짜고, 플랫폼마다 얇은 연결 계층(바인딩)만 따로 붙인다. WebRTC가 정확히 이 구조로 동작합니다.

이 글은 그 구조를 처음부터 끝까지 정리합니다. 왜 코어가 C++인지, 네이티브 코드와 크로스 컴파일이 정확히 뭔지, .a.so의 진짜 구분 기준은 무엇인지(여기서 흔한 오해를 정정합니다), Android는 JNI로·iOS는 Objective-C++로 어떻게 C++를 부르는지를 차례로 짚습니다. 마지막으로 이 구조의 트레이드오프(업데이트하려면 앱을 다시 빌드해야 한다)까지 다룹니다.


0. 핵심 명제 — 코어는 하나, 연결 계층은 플랫폼마다

WebRTC 같은 실시간 엔진은 C++로 작성된 단일 코어를 iOS와 Android가 공유한다. 두 OS가 코드를 따로 갖는 게 아니라, 같은 C++ 소스를 각 플랫폼의 CPU 명령어 집합(ABI)에 맞춰 크로스 컴파일하고, 그 위에 OS별 언어(Android는 Java/Kotlin, iOS는 Swift/Objective-C)가 C++를 호출할 수 있도록 얇은 바인딩 계층만 따로 붙인다. Android의 바인딩은 JNI라는 표준 규격을 거치고, iOS는 Objective-C++(.mm) 또는 Swift-C++ interop으로 언어 차원에서 직접 연결한다.

흔한 오해와 정정부터 박아두겠습니다. 이 표 하나가 이 글 전체의 요약입니다.

오해정확한 이해
iOS와 Android는 RTC 코드를 각자 따로 갖고 있다❌ 같은 C++ 코어를 공유. 다른 건 컴파일 타깃과 바인딩 계층뿐
.so는 Android(리눅스)용, .a는 iOS(유닉스)용 기계어다OS 구분이 아니다. .a=정적/​.so=동적 — 링킹 방식의 차이. 두 OS 모두 둘 다 가능
ARM 칩이면 다 같으니 .so 하나로 모든 안드로이드폰에서 돈다❌ ABI(armeabi-v7a / arm64-v8a / x86_64…)가 다르면 호환 안 됨. APK에 여러 .so를 담음
C++ 코어는 OS가 알아서 자동으로 업데이트해준다❌ 코어는 앱 바이너리에 박혀 배포됨. 업데이트 = 앱 재빌드 + 스토어 재배포
iOS도 JNI 같은 규격이 필요하다❌ iOS는 JVM이 없어 JNI 불필요. ObjC++/Swift가 C++를 언어 차원에서 직접 호출

1. 왜 코어가 C++인가 — 성능, GC 없음, 멀티플랫폼

실시간 미디어 엔진(WebRTC, 코덱, 에코 캔슬러, 지터 버퍼)은 밀리초 단위로 예측 가능한 성능이 생명입니다. 오디오는 보통 10ms 단위 프레임을, 비디오는 16ms(60fps)~33ms(30fps) 단위로 끊김 없이 처리해야 합니다. 이 요구가 언어 선택을 거의 강제합니다.

C++를 고르는 세 가지 이유

🟢 1. 네이티브 성능 — 하드웨어에 직접 닿는다 C++는 가상 머신이나 인터프리터를 거치지 않고 CPU 기계어로 직접 컴파일됩니다. 코덱의 DSP 루프, SIMD 벡터 연산(NEON/SSE), 메모리 정렬 같은 저수준 최적화를 그대로 표현할 수 있습니다.

🟢 2. GC(Garbage Collector) 없음 — 예측 가능한 지연 이게 핵심입니다. Java/Kotlin(JVM)이나 Swift가 아닌 이유.

🔴 GC 있는 언어 (예: JVM)
   오디오 프레임 처리 중...
   → GC가 갑자기 끼어들어 "Stop-the-world" 일시정지
   → 수 ms ~ 수십 ms 멈춤 (Major GC)
   → 그 사이 오디오 프레임 마감 시간(10ms) 놓침
   → "뚝뚝" 끊김 (audio glitch)

🟢 GC 없는 C++ (수동 메모리 관리)
   오디오 프레임 처리 중...
   → 개발자가 메모리 수명을 직접 통제
   → 예측 못한 일시정지 없음
   → 프레임 마감 시간을 안정적으로 지킴

정정: Swift는 GC가 아니라 ARC(Automatic Reference Counting)를 씁니다. ARC는 "stop-the-world" 일시정지가 없다는 점에서 JVM의 GC보다 실시간에 유리하지만, 참조 카운트 갱신 비용·순환 참조 관리·iOS 전용이라는 제약 때문에 멀티플랫폼 공용 코어로는 여전히 C++가 표준입니다. 즉 "GC 없음"이 C++만의 독점 특성은 아니지만, GC 없음 + 멀티플랫폼 + 저수준 제어를 동시에 만족하는 건 C/C++가 사실상 유일합니다.

🟢 3. 멀티플랫폼 단일 코어 — Write Once, Compile Everywhere C++ 소스는 ISO 표준이라 컴파일러만 있으면 어느 플랫폼이든 같은 코드를 빌드할 수 있습니다. 코어 알고리즘(코덱, 네트워크 스택, 에코 캔슬러)을 한 번 짜고 iOS·Android·Windows·macOS·Linux 모두에서 재사용합니다. WebRTC 코드베이스가 약 90% 이상 C++인 이유가 여기에 있습니다. [NEEDS VERIFICATION: GitHub 미러 기준 언어 비중은 시점에 따라 변동]

⚠️ 트레이드오프: C++는 메모리를 수동으로 관리하므로 댕글링 포인터·버퍼 오버플로우·메모리 누수 위험이 큽니다. "예측 가능한 성능"을 얻는 대가로 "메모리 안전성을 개발자가 책임진다". 그래서 최근에는 Rust 같은 대안도 부상하지만, 기존 C++ 코어의 거대한 자산과 생태계 때문에 RTC 엔진의 주류는 여전히 C++입니다.

언어 선택 비교

언어성능일시정지 위험멀티플랫폼 코어RTC 코어로
C++🟢 네이티브🟢 GC 없음🟢 전 플랫폼✅ 주류
Java/Kotlin (JVM)⚠️ JIT🔴 GC stop-the-world❌ JVM 종속
Swift🟢 네이티브🟢 ARC (GC 아님)❌ 사실상 Apple 전용❌ 코어로는 부적합
Rust🟢 네이티브🟢 GC 없음🟢 전 플랫폼⚠️ 부상 중, 자산 부족

2. 네이티브 코드 · 컴파일 · 크로스 컴파일 · ARM ABI

코어가 C++라는 건 알았습니다. 그런데 "C++ 소스"가 곧바로 폰에서 도는 건 아닙니다. 사람이 읽는 텍스트일 뿐이고, CPU가 실행하려면 기계어(machine code)로 번역해야 합니다. 이 번역이 컴파일입니다.

네이티브 코드란?

네이티브 코드 = 특정 CPU가 직접 실행할 수 있는 기계어 명령어. 중간 가상 머신(JVM 등)을 거치지 않고 하드웨어에서 바로 도는 코드.

C++ 소스 (.cpp, 사람이 읽는 텍스트)
      │  컴파일 (clang/gcc)
      ▼
기계어 (특정 CPU 전용 0/1 명령어)
      │
      ▼
CPU가 직접 실행 ← "네이티브"

크로스 컴파일이란?

여기서 모바일 개발 특유의 문제가 생깁니다. 개발자는 Mac이나 PC에서 코드를 짜지만, 그 코드는 폰의 ARM CPU에서 돌아야 합니다. Mac의 CPU와 폰의 CPU는 명령어 집합이 다릅니다. 그래서 "내 컴퓨터와 다른 종류의 CPU용 기계어를 만드는" 컴파일이 필요합니다.

크로스 컴파일(Cross-compilation): 컴파일러가 실행되는 플랫폼(Host)과 다른 플랫폼(Target)의 실행 코드를 생성하는 것.

  • Host = 컴파일러가 도는 곳 (보통 개발자의 Mac/PC)
  • Target = 생성된 코드가 실행될 곳 (폰의 ARM 기기)
[Host: 개발자 Mac (Apple Silicon 또는 Intel)]
        │
        │  Android NDK / Xcode 의 크로스 컴파일러
        │  "내 CPU 말고 ARM 폰용 기계어를 만들어줘"
        ▼
[Target용 기계어 산출물]
        │
        ▼
[폰의 ARM CPU에서 실행]

비유로 풀면: 한국에서 일본어 책을 인쇄하는 것과 같습니다. 인쇄기(Host)는 한국에 있지만 결과물(일본어 책)은 일본 독자(Target)를 위한 것입니다. 기술적 실체로 돌아오면 — Mac의 clang이 ARM64 명령어를 출력하면, 그 산출물은 Mac에서는 못 돌고 폰에서만 돈다. Android는 NDK(Native Development Kit), iOS는 Xcode 툴체인이 이 크로스 컴파일러를 내장하고 있습니다.

ARM ISA는 같은데 왜 하나의 바이너리로 안 되나 — ABI 문제

흔한 오해를 정정합니다.

정정: "스냅드래곤·미디어텍·Apple Silicon이 다 ARM이니까 ARM 기계어 .so 하나면 모든 안드로이드폰에서 돈다" → FALSE.

ARM ISA(명령어 집합 구조)를 공유하더라도 ABI(Application Binary Interface)가 다르면 호환되지 않습니다. ABI는 명령어뿐 아니라 함수 호출 규약, 레지스터 사용법, 데이터 정렬, 32비트/64비트 여부까지 포함한 "바이너리 수준의 약속"입니다.

Android가 지원하는 주요 ABI:

ABI설명비트
armeabi-v7a32비트 ARM (Thumb-2)32-bit
arm64-v8a64비트 ARM (AArch64) — 현재 권장64-bit
x8632비트 인텔 (에뮬레이터 등)32-bit
x86_6464비트 인텔 (에뮬레이터, 일부 기기)64-bit

하나의 .so 바이너리는 하나의 ABI에만 호환됩니다. ARMv8 칩(스냅드래곤 등)은 AArch32 호환 모드로 armeabi-v7a 코드를 실행할 수는 있지만, 이는 64비트 칩에서 32비트 코드를 억지로 돌리는 셈이라 비효율적입니다. 그래서 실무에서는 multi-ABI로 빌드하여 APK 안에 여러 .so를 담고, 설치 시 기기에 맞는 것만 골라 씁니다.

my-app.apk
└── lib/
    ├── arm64-v8a/libwebrtc.so      ← 최신 64비트 폰
    ├── armeabi-v7a/libwebrtc.so    ← 구형 32비트 폰
    └── x86_64/libwebrtc.so         ← 에뮬레이터
        ↑ 같은 C++ 소스를 ABI별로 각각 크로스 컴파일한 결과물
# Android NDK 설정 예 (개념)
APP_ABI := arm64-v8a armeabi-v7a x86_64
# → 같은 C++ 소스를 세 ABI로 각각 빌드 → APK에 세 개의 .so 포함

⚠️ 따라서 "ARM이면 다 같다"가 아니라, 같은 C++ 소스 → ABI 개수만큼 크로스 컴파일 → 각각 별도 바이너리가 정확한 그림입니다.


3. ★중요 정정 — .a(static) vs .so(shared)는 OS 구분이 아니다

이 글에서 가장 자주 틀리는 지점입니다. 명확히 정정하겠습니다.

정정: ".so는 Android(리눅스)용, .a는 iOS(유닉스)용 기계어 파일이다" → FALSE.

.a.so의 구분 기준은 운영체제가 아니라 링킹(linking) 방식입니다. 둘 다 "컴파일된 기계어 묶음"이라는 점은 같고, 그것을 앱에 언제 결합하느냐가 다릅니다.

Static vs Dynamic — 진짜 구분 기준

항목.a (Static Archive).so (Shared Object)
결합 시점컴파일/링크 시점런타임(실행 중) 로드
코드 위치앱 바이너리 안에 복사별도 파일로 존재, 실행 시 로드
앱 크기커짐 (코드 포함)작아짐 (외부 참조)
공유불가 (각 앱이 복사본)여러 프로세스가 공유 가능
개념의 소속OS 무관 — 링킹 방식OS 무관 — 링킹 방식
🔗 Static (.a) — 컴파일할 때 코드를 앱 안으로 복사
  [libcore.a] ──복사──▶ [내 앱 바이너리(코드 포함)]
                          → 실행 시 외부 파일 불필요

🔗 Dynamic (.so) — 실행할 때 외부 파일을 로드
  [내 앱 바이너리] ──런타임 로드──▶ [libcore.so]
                          → 실행 시 .so 파일이 옆에 있어야 함

비유: .a는 책 내용을 통째로 복사해서 내 보고서에 붙여넣는 것(보고서가 두꺼워지지만 독립적), .so참고문헌으로 링크만 걸어두는 것(보고서는 얇지만 그 문헌이 옆에 있어야 함). 기술적 실체 — Static은 링커가 .a 안의 필요한 오브젝트를 앱 실행 파일에 직접 병합하고, Dynamic은 실행 시 동적 링커가 .so를 메모리에 매핑합니다.

두 OS 모두 둘 다 지원한다 — 이게 핵심

Static (.a)Dynamic비고
Android✅ 지원 (다른 라이브러리에 링크).so (NDK 기본 산출물)NDK가 C/C++ 소스로 .so를 빌드
iOS✅ 지원 (과거 표준).dylib / .framework현대는 .framework/XCFramework 권장. .framework는 패키징 형식일 뿐, 정적/동적은 빌드 설정으로 결정됨

Android NDK 공식 설명을 그대로 옮기면:

  • Native Libraries (.so files) — NDK가 C/C++ 소스에서 빌드하는 동적 라이브러리
  • Static libraries (.a files) — 다른 라이브러리에 링크해 넣을 수 있는 정적 라이브러리

즉 Android도 .a를 쓰고, iOS도 과거엔 .a 정적 라이브러리를 요구했습니다. 현대 iOS 워크플로우는 동적 .framework(또는 여러 플랫폼/아키텍처를 묶은 XCFramework)를 권장하지만, .a와 정적 프레임워크도 여전히 지원합니다.

✅ 외워둘 한 줄: .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 래퍼

🔧 정정: Google은 WebRTC M80(약 2020년) 이후 모바일 prebuilt 바이너리(GoogleWebRTC CocoaPod 등) 공식 배포를 중단했고, 현재 공식 경로는 소스 빌드입니다. XCFramework/AAR prebuilt는 대개 커뮤니티(stasel/WebRTC 등)나 벤더(Agora 등) 빌드로 유통됩니다. (출처: WebRTC Native Code — iOS)

        같은 C++ 코어 소스 (libwebrtc)
       /                            \
   크로스 컴파일 (iOS 타깃)       크로스 컴파일 (Android 타깃)
       │                            │
   WebRTC.framework /            libjingle_peerconnection.so
   XCFramework                   + .aar(Java/Kotlin 래퍼 포함)
       │                            │
   [iOS 앱에 링크]               [Android 앱에 링크]

⚠️ WebRTC는 오픈소스라 배포 형태가 다양합니다. Google 소스를 빌드하면 iOS는 XCFramework, Android는 AAR+.so 형태로 산출되며, 이를 Agora·Firebase 등이 자체 래퍼와 함께 SDK로 재배포합니다. 그래서 "정확히 어떤 파일 확장자냐"는 누가 배포했느냐에 따라 달라집니다.


4. Android의 연결 — JNI로 Java/Kotlin ↔ C++ 잇기

Android 앱은 Java/Kotlin으로 작성되고 JVM(정확히는 ART, Android Runtime) 위에서 돕니다. 그런데 WebRTC 코어는 C++ 네이티브 코드입니다. JVM 세계의 Kotlin이 JVM 밖의 C++ 함수를 어떻게 부를까요? 이 다리가 JNI입니다.

JNI란?

JNI(Java Native Interface): Java/Kotlin 코드와 C/C++ 네이티브 코드가 서로 호출할 수 있게 해주는 표준 규격.

판정: TRUE — JNI는 Sun Microsystems가 설계하고 현재 Oracle(Sun 인수)이 관리하는 Java 표준입니다. 공식 사양에 따르면, JDK 1.0의 native 인터페이스, Netscape JRI, Microsoft RNI 등 서로 호환되지 않던 여러 native 인터페이스를 하나로 통일하기 위해 만들어졌습니다. 모든 JVM 구현(Oracle, OpenJDK, Android ART)에서 지원됩니다. Android NDK 문서 표현으로는 "Java와 C/C++ 컴포넌트가 서로 통신할 수 있게 하는 다리(bridge)".

JNI가 작동하는 3단계

[Kotlin/Java 코드]
   │  ① native 키워드로 "이 함수는 C++에 있다" 선언
   ▼
[JNI 경계]  ← Sun/Oracle이 정한 표준 규약
   │  ② System.loadLibrary()로 .so 로드
   │  ③ 함수 이름 규칙(JNIEXPORT)으로 C++ 함수 매핑
   ▼
[C++ 네이티브 코드 (.so 안)]

단계별 코드

①② Kotlin: external 선언 + .so 로드

①(external 선언)과 ②(loadLibrary)는 같은 Kotlin 블록 안에서 함께 이뤄집니다.

class WebRtcEngine {
    // ① "이 함수의 실제 구현은 C++ .so 안에 있다"는 선언
    external fun startCall(channelId: String): Int

    companion object {
        init {
            // ② 앱 실행 시 .so를 메모리에 로드
            System.loadLibrary("webrtc_core")   // → libwebrtc_core.so
        }
    }
}

Java로 쓰면 public native int startCall(String channelId); — 키워드만 native로 다를 뿐 같은 개념입니다.

③ C++: 이름 규칙으로 함수 구현

JNI는 함수 이름을 Java_<패키지>_<클래스>_<메서드> 규칙으로 매핑합니다. 이름이 정확히 맞아야 런타임이 연결합니다.

#include <jni.h>

extern "C"   // C++ 이름 맹글링 방지 (JNI는 C 심볼 규약을 기대)
JNIEXPORT jint JNICALL
Java_com_example_WebRtcEngine_startCall(
    JNIEnv* env,        // JNI 환경 포인터 (JVM과 통신하는 핸들)
    jobject thiz,       // 호출한 Kotlin 객체 (this)
    jstring channelId)  // Kotlin String → JNI jstring
{
    // jstring을 C 문자열로 변환
    const char* id = env->GetStringUTFChars(channelId, nullptr);

    int result = WebRtcCore::StartCall(id);   // 실제 C++ 코어 호출

    env->ReleaseStringUTFChars(channelId, id);  // 수동 해제 (메모리 관리)
    return result;   // int → jint 로 JVM에 반환
}

핵심 포인트:

  • extern "C" — C++ 컴파일러는 함수 이름을 변형(name mangling)하는데, JNI는 변형 안 된 C 심볼을 기대하므로 이걸로 막습니다.
  • JNIEXPORT / JNICALL — JNI가 인식하는 함수임을 표시하는 매크로.
  • 타입 변환 — Kotlin의 String은 JVM 객체라 C++의 char*로 쓰려면 GetStringUTFChars로 변환해야 합니다. JVM 세계와 네이티브 세계는 데이터 표현이 다르기 때문입니다.

⚠️ JNI의 대가: 모든 데이터가 JVM↔네이티브 경계를 넘을 때 마샬링(변환) 비용이 듭니다. 객체 변환, 참조 관리(Local/Global Reference), 예외 전파를 직접 신경 써야 합니다. 그래서 JNI 호출은 "자주 넘나들수록 비싸지므로" 경계를 굵게(coarse-grained) 설계하는 게 정석입니다.


5. iOS의 연결 — Objective-C++(.mm)와 Swift-C++ Interop

iOS에는 JVM이 없습니다. 앱은 Swift나 Objective-C로 작성됩니다. 그래서 Android식 JNI 같은 별도 바인딩 규격이 필요 없습니다. C++와의 연결이 언어 차원에서 더 매끄럽습니다.

판정: TRUE — iOS는 JVM이 없어 Swift/Objective-C를 쓰고, C++ 연결은 Objective-C++(.mm 파일) 또는 Swift-C++ 직접 interop으로 이뤄집니다. Java-C++ 매핑 계층이 필요한 JNI보다 seamless합니다.

Objective-C++ — 한 파일에서 ObjC와 C++ 혼용

.mm 확장자 파일은 Objective-C++ 컴파일 단위입니다. 같은 파일 안에서 Objective-C 객체와 C++ 객체를 자유롭게 섞을 수 있습니다. JNI 같은 별도 규약·이름 규칙·타입 변환 함수가 필요 없습니다 — 컴파일러가 두 언어를 함께 이해하기 때문입니다.

// WebRtcEngine.mm   ← .mm = Objective-C++ (ObjC + C++ 동시 가능)
#import "WebRtcEngine.h"
#include "webrtc_core.h"     // C++ 헤더를 그대로 include

@implementation WebRtcEngine

- (int)startCall:(NSString *)channelId {
    // NSString(ObjC) → std::string(C++) 변환은 한 줄
    std::string id = std::string([channelId UTF8String]);

    // C++ 코어를 직접 호출 — 별도 바인딩 규약 불필요
    return WebRtcCore::StartCall(id);
}

@end

JNI 코드(섹션 4)와 비교해보면 차이가 분명합니다: extern "C", JNIEXPORT, JNIEnv*, GetStringUTFChars/ReleaseStringUTFChars 같은 의식(ceremony)이 전혀 없습니다. C++ 헤더를 include하고 함수를 바로 호출하면 끝입니다.

Swift에서 C++ 부르기 — 두 갈래

[Swift 앱 코드]
   │
   ├─ (전통) Swift → ObjC++ 래퍼(.mm) → C++
   │         Swift는 C++를 직접 못 보므로 ObjC 래퍼 경유
   │
   └─ (2023+) Swift → C++ 직접 interop
             Swift-C++ interoperability 활성화 시 C++ 직접 호출

전통적으로 Swift는 C++를 직접 보지 못해 Objective-C++ 래퍼를 한 겹 두고, Swift는 그 ObjC 인터페이스만 봤습니다. 하지만 Apple은 WWDC 2023 무렵부터 direct Swift-C++ interoperability를 도입해, 설정을 켜면 Swift가 C++ 타입·함수를 직접 다룰 수 있게 됐습니다. [NEEDS VERIFICATION: 직접 interop은 C++ 기능별로 지원 범위·성숙도가 다르므로 프로젝트에서 실제 검증 필요]

Android(JNI) vs iOS(ObjC++/Swift) 비교

항목AndroidiOS
앱 언어Java / KotlinSwift / Objective-C
런타임ART(JVM류) — VM 있음네이티브 — VM 없음
C++ 연결 방식JNI (표준 규격 계층)ObjC++(.mm) / Swift-C++ interop
별도 규약 필요?✅ 필요 (JNIEXPORT, 이름 규칙)❌ 언어 차원 지원
데이터 변환수동 마샬링(GetStringUTFChars 등)한 줄 변환 또는 자동
표현"다리를 놓는다(bridge)""같이 컴파일한다"

✅ 한 줄: Android는 JVM↔네이티브 경계가 있어 JNI라는 다리가 필요하고, iOS는 경계가 없어 컴파일러가 ObjC++/Swift와 C++를 한 번에 처리한다. iOS 쪽이 더 매끄러운 건 "규격이 좋아서"가 아니라 "넘을 경계 자체가 없어서"다.


6. 래퍼(Wrapper) — 앱 개발자가 실제로 만지는 것

여기까지 보면 의문이 생깁니다. "그럼 RTC 앱 만들 때 매번 JNI나 .mm을 직접 짜야 하나?" 아닙니다. 그 위에 래퍼(Wrapper) 계층이 있습니다.

래퍼: 저수준 C++ 코어를 각 플랫폼의 관용적(idiomatic) API로 감싼 얇은 계층. 앱 개발자는 Kotlin/Swift 객체만 다루고, 내부의 C++ 호출은 래퍼가 숨긴다.

┌──────────────────────────────────────────────┐
│ 앱 개발자 코드 (Kotlin / Swift)               │
│   engine.startCall("room-123")  ← 이것만 만짐 │
├──────────────────────────────────────────────┤
│ 플랫폼 래퍼 (Kotlin 클래스 / Swift 클래스)    │
│   - Android: JNI 호출을 감싼 Kotlin API       │
│   - iOS:     ObjC++/Swift로 감싼 Swift API    │
├──────────────────────────────────────────────┤
│ 바인딩 계층                                    │
│   - Android: JNI (Java_..._startCall)         │
│   - iOS:     .mm / Swift-C++ interop          │
├──────────────────────────────────────────────┤
│ C++ 코어 (libwebrtc) ← 양 플랫폼 공유           │
│   코덱 · 네트워크 · 에코캔슬러 · 지터버퍼       │
└──────────────────────────────────────────────┘

각 계층의 역할:

계층역할누가 작성
앱 코드비즈니스 로직, 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. 업데이트 = 앱 재빌드 + 스토어 재배포

마지막으로 이 구조의 가장 중요한 운영상 트레이드오프입니다.

정정: "C++ 코어는 OS나 서버가 알아서 업데이트해준다" → FALSE.

C++ 코어(.so/.framework)는 앱 바이너리 안에 포함되어 배포됩니다. 서버에서 동적으로 받아오는 웹 자바스크립트와 근본적으로 다릅니다.

🌐 웹 (브라우저 WebRTC)
   브라우저가 WebRTC 엔진을 내장 + 자동 업데이트
   → 사용자가 Chrome 업데이트하면 엔진도 최신화
   → 앱(웹사이트)은 JS만 갱신하면 끝

📱 네이티브 (C++ 코어 내장)
   C++ 코어가 앱 바이너리에 박혀 있음
   → 코어 버전 올리려면:
     ① 새 SDK 버전 의존성 교체
     ② 앱 재빌드 (크로스 컴파일 다시)
     ③ 앱스토어/플레이스토어 재심사·재배포
     ④ 사용자가 앱 업데이트해야 적용

영향 비교

항목웹(브라우저 내장)네이티브(C++ 코어 내장)
코어 업데이트 경로브라우저 자동 / JS 즉시앱 재빌드 + 스토어 재배포
반영 속도빠름느림 (심사 + 사용자 업데이트 대기)
버전 파편화적음🔴 큼 (구버전 앱 사용자 잔존)
긴급 패치즉시 가능스토어 심사 주기에 묶임

⚠️ 실무 함의: 네이티브 앱은 사용자마다 다른 SDK 버전을 들고 있을 수 있습니다. 보안 취약점이나 코덱 버그를 고쳐도, 사용자가 앱을 업데이트하기 전엔 구버전 코어가 그대로 돕니다. 그래서 RTC SDK 도입 시 버전 강제 업데이트 정책(예: 특정 버전 미만은 접속 차단)이나 서버 측 호환성 유지를 미리 설계해야 합니다.

이것이 #43에서 다룬 "네이티브는 자체 코덱을 쓰지만 웹은 브라우저 표준에 묶인다"의 또 다른 측면입니다. 네이티브는 제어권을 얻는 대신 배포 민첩성을 잃습니다.


8. 전체 그림 한 장 — 하나의 C++ 코어가 두 OS로

                    ┌─────────────────────────┐
                    │   C++ 코어 소스 (단일)   │
                    │  코덱·네트워크·AEC·버퍼  │
                    └────────────┬────────────┘
                                 │
              ┌──────────────────┴──────────────────┐
   크로스 컴파일 (NDK)                    크로스 컴파일 (Xcode)
              │                                      │
   ABI별 .so (arm64-v8a 등)            XCFramework/.framework
              │                                      │
        JNI 바인딩                         ObjC++(.mm)/Swift interop
     (Java_..._fn, JNIEXPORT)            (헤더 include 후 직접 호출)
              │                                      │
      Kotlin/Java 래퍼                        Swift 래퍼
              │                                      │
        [Android 앱]                            [iOS 앱]
              │                                      │
        스토어 배포 ──── 업데이트하려면 재빌드 ──── 스토어 배포

9. 암기 카드 — 한 줄 요약

개념한 줄 요약
왜 C++네이티브 성능 + GC 없음(예측 가능) + 멀티플랫폼 단일 코어
네이티브 코드CPU가 직접 실행하는 기계어, VM 안 거침
크로스 컴파일Host(Mac/PC)에서 Target(폰 ARM)용 기계어 생성
ABIARM ISA 같아도 ABI 다르면 비호환 → APK에 여러 .so
.a vs .so⭐ OS 구분 ❌ / 정적(컴파일 시 포함) vs 동적(런타임 로드)
WebRTC 배포iOS=XCFramework/.framework, Android=.aar+.so 형태가 전형적 (실제 확장자는 배포 주체에 따라 다름, 코어는 같은 C++)
JNIJava/Kotlin↔C++ 다리. Sun/Oracle 표준. native 키워드 + loadLibrary + JNIEXPORT
iOS 연결JVM 없음 → ObjC++(.mm)/Swift interop, JNI 불필요 (더 매끄러움)
래퍼앱 개발자는 Kotlin/Swift만 만짐, 바인딩·코어는 SDK가 숨김
업데이트코어가 앱에 박힘 → 재빌드 + 스토어 재배포 필요
하나의 C++ 코어
   → ABI별 크로스 컴파일
   → Android: .so + JNI + Kotlin 래퍼
   → iOS:     .framework + ObjC++/Swift + Swift 래퍼
   → 업데이트는 앱 재빌드로만

10. 한 줄 결론

iOS와 Android는 RTC 코드를 따로 갖지 않는다. 같은 C++ 코어를 ABI마다 크로스 컴파일하고, 그 위에 OS별 연결 계층만 얇게 붙인다 — Android는 JVM 경계가 있어 JNI라는 표준 다리를, iOS는 경계가 없어 ObjC++/Swift interop을 쓴다. 핵심 정정 둘: (1) .a/.so는 iOS/Android 구분이 아니라 정적/동적 링킹의 구분이며 두 OS 모두 둘 다 쓴다. (2) ARM이라고 바이너리 하나로 다 도는 게 아니라 ABI마다 별도 .so가 필요하다. 이 구조의 대가는 배포 민첩성 — 코어가 앱에 박혀 있어 업데이트하려면 앱을 재빌드해 스토어에 다시 올려야 한다.


관련 글

참고 자료

© 2026 Frank Kim. All rights reserved.