C, Objective-C, Swift 라이브러리 가져오기
C, Objective-C, Swift 라이브러리 가져오기
Kotlin/Native는 C(C interop)와 Objective-C(Objective-C interop) 라이브러리를 가져올 수 있는 기능을 제공해요. 순수 Swift 라이브러리(Swift 라이브러리 가져오기)를 Kotlin/Native 프로젝트에 가져오는 문제도 우회할 수 있습니다.
본문
C와 Objective-C 라이브러리 가져오기의 안정성
C와 Objective-C 라이브러리 가져오기 지원은 현재 베타(Kotlin/Native) 상태입니다.
베타 상태인 주된 이유 중 하나는 C와 Objective-C 라이브러리를 사용하면 여러분의 코드가 다양한 Kotlin 버전, 의존성, Xcode 버전과의 호환성에 영향을 받을 수 있기 때문이에요. 이 가이드는 실제로 자주 발생하는 호환성 문제, 일부 경우에만 발생하는 문제, 그리고 잠재적으로 발생 가능한 문제까지 정리합니다.
간단히 하기 위해 C와 Objective-C 라이브러리(네이티브 라이브러리)를 다음과 같이 나눌게요:
- 플랫폼 라이브러리(#platform-libraries): 각 플랫폼의 "시스템" 네이티브 라이브러리에 접근하기 위해 Kotlin이 기본으로 제공하는 라이브러리.
- 서드파티 라이브러리(#third-party-libraries): Kotlin에서 사용하려면 추가 구성이 필요한 그 외 모든 네이티브 라이브러리.
이 두 종류의 네이티브 라이브러리는 서로 다른 호환성 특성을 갖습니다.
플랫폼 라이브러리
플랫폼 라이브러리(native-platform-libs)는 Kotlin/Native 컴파일러와 함께 배포됩니다. 따라서 프로젝트에서 다른 Kotlin 버전을 사용하면 다른 버전의 플랫폼 라이브러리를 얻게 됩니다. Apple 타깃(iOS 등)의 경우 플랫폼 라이브러리는 특정 컴파일러 버전이 지원하는 Xcode 버전을 기반으로 생성됩니다.
Xcode SDK와 함께 배포되는 네이티브 라이브러리 API는 Xcode 버전마다 바뀝니다. 이런 변경이 네이티브 언어 내에서 소스·바이너리 호환이 되더라도, 상호운용 구현 때문에 Kotlin에서는 호환성이 깨질 수 있어요.
결과적으로 프로젝트의 Kotlin 버전을 업데이트하면 플랫폼 라이브러리에 호환성이 깨지는 변경이 생길 수 있습니다. 이는 두 경우에 문제가 될 수 있어요:
- 프로젝트의 소스 코드 컴파일에 영향을 주는 플랫폼 라이브러리의 소스 호환성 파괴 변경이 있는 경우. 보통은 고치기 쉽습니다.
- 일부 의존성에 영향을 주는 플랫폼 라이브러리의 바이너리 호환성 파괴 변경이 있는 경우. 이때는 쉬운 우회책이 없고, 라이브러리 개발자가 Kotlin 버전을 업데이트하는 등 자기 쪽에서 고쳐 주기를 기다려야 해요.
이런 바이너리 비호환성은 링크 경고와 런타임 예외로 나타납니다. 컴파일 시간에 감지하고 싶다면 -Xpartial-linkage-loglevel=ERROR(라이브러리 링크) 컴파일러 옵션으로 경고를 오류로 올릴 수 있어요.
JetBrains 팀이 플랫폼 라이브러리를 생성할 때 사용하는 Xcode 버전을 업데이트하면 호환성 파괴 변경을 피하기 위해 합리적인 노력을 기울입니다. 파괴적 변경이 발생할 가능성이 있으면 영향 분석을 수행하고, 특정 변경을 무시하거나(영향을 받는 API가 흔히 쓰이지 않기 때문에) 임시 수정을 적용합니다.
플랫폼 라이브러리의 파괴적 변경의 또 다른 잠재적 원인은 네이티브 API를 Kotlin으로 변환하는 알고리즘의 변경입니다. JetBrains 팀은 이런 경우에도 파괴적 변경을 피하려는 합리적인 노력을 합니다.
플랫폼 라이브러리에서 새로운 Objective-C 클래스 사용하기
Kotlin 컴파일러는 배포 타깃(deployment target)에서 사용할 수 없는 Objective-C 클래스를 사용하는 것을 막지 않습니다.
예를 들어 배포 타깃이 iOS 17.0인데 iOS 18.0에서만 나타난 클래스를 사용하면, 컴파일러는 경고하지 않고 iOS 17.0을 가진 디바이스에서 앱이 시작 중 크래시할 수 있어요. 게다가 실행 경로가 그 사용 지점에 도달하지 않더라도 크래시가 발생하므로, 버전 체크로 보호하는 것만으로는 부족합니다.
자세한 내용은 Strong linking을 참고하세요.
서드파티 라이브러리
시스템 플랫폼 라이브러리 외에도 Kotlin/Native는 서드파티 네이티브 라이브러리 가져오기를 허용합니다. 예를 들어 CocoaPods 통합을 쓰거나 cinterops 구성을 설정할 수 있어요.
Xcode 버전이 일치하지 않는 라이브러리 가져오기
서드파티 네이티브 라이브러리를 가져오면 다양한 Xcode 버전과의 호환성 문제가 생길 수 있어요.
네이티브 라이브러리를 처리할 때 컴파일러는 보통 로컬에 설치된 Xcode의 헤더 파일을 사용합니다. 거의 모든 네이티브 라이브러리 헤더가 Xcode에서 오는 "표준" 헤더(예: stdint.h)를 import하기 때문이에요.
그래서 Xcode 버전은 네이티브 라이브러리의 Kotlin 가져오기에 영향을 줍니다. 이것은 또한 서드파티 네이티브 라이브러리를 사용할 때 Mac이 아닌 호스트에서 Apple 타깃을 크로스 컴파일하는 것(Apple 타깃 컴파일)이 여전히 불가능한 이유 중 하나이기도 해요.
모든 Kotlin 버전은 단일 Xcode 버전과 가장 잘 호환됩니다. 이는 권장 버전이며 해당 Kotlin 버전에 대해 가장 많이 테스트됩니다. 특정 Xcode 버전과의 호환성은 호환성 테이블에서 확인하세요.
더 새롭거나 오래된 Xcode 버전을 사용하는 것도 가능하지만 문제가 생길 수 있고, 보통 서드파티 네이티브 라이브러리 가져오기에 영향을 줍니다.
권장보다 새로운 Xcode 버전
권장보다 새로운 Xcode 버전을 사용하면 일부 Kotlin 기능이 깨질 수 있어요. 특히 서드파티 네이티브 라이브러리 가져오기가 가장 큰 영향을 받습니다. 지원되지 않는 Xcode 버전에서는 종종 아예 동작하지 않습니다.
권장보다 오래된 Xcode 버전
보통 Kotlin은 오래된 Xcode 버전과 잘 동작합니다. 가끔 문제가 생기는데, 대부분 다음과 같은 결과를 낳습니다:
- KT-71694처럼 Kotlin API가 존재하지 않는 타입을 참조하는 경우.
- 시스템 라이브러리의 타입이 네이티브 라이브러리의 Kotlin API에 포함되는 경우. 이 경우 프로젝트는 성공적으로 컴파일되지만 시스템 네이티브 타입이 네이티브 라이브러리 패키지에 추가됩니다. 예를 들어 이후 IDE 자동 완성에서 이 타입이 예상치 못하게 보일 수 있어요.
오래된 Xcode 버전으로 Kotlin 라이브러리가 성공적으로 컴파일된다면, Kotlin 라이브러리 API에서 서드파티 라이브러리의 타입을 사용하지 않는 한(라이브러리 API에서 네이티브 타입 사용) 게시해도 안전합니다.
전이적 서드파티 네이티브 의존성 사용하기
프로젝트의 Kotlin 라이브러리가 구현의 일부로 서드파티 네이티브 라이브러리를 가져오면, 여러분의 프로젝트도 그 네이티브 라이브러리에 접근할 수 있게 됩니다. Kotlin/Native는 api와 implementation 의존성 타입을 구분하지 않아 네이티브 라이브러리가 항상 api 의존성이 되기 때문이에요.
이런 전이적 네이티브 의존성 사용은 더 많은 호환성 문제가 생기기 쉽습니다. 예를 들어 Kotlin 라이브러리 개발자가 한 변경을 하면 네이티브 라이브러리의 Kotlin 표현이 호환되지 않게 되어, Kotlin 라이브러리를 업데이트할 때 호환성 문제가 생길 수 있어요.
그러므로 전이적 의존성에 기대기보다는 같은 네이티브 라이브러리와의 상호운용을 직접 구성하세요. 이를 위해 네이티브 라이브러리에 다른 패키지 이름을 사용하세요. 커스텀 패키지 이름 사용처럼 말이죠. 그러면 호환성 문제를 막을 수 있어요.
라이브러리 API에서 네이티브 타입 사용하기
Kotlin 라이브러리를 게시한다면 라이브러리 API의 네이티브 타입에 주의하세요. 이런 사용은 호환성 및 기타 문제를 해결하려고 향후 깨질 것으로 예상되며, 라이브러리 사용자에게 영향을 줄 거예요.
네이티브 타입을 라이브러리 API에서 사용해야 하는 경우도 있어요. 예를 들어 Kotlin 라이브러리가 기본적으로 네이티브 라이브러리에 확장을 제공하는 경우처럼 라이브러리의 목적에 필요한 경우죠. 그런 경우가 아니라면 라이브러리 API에서 네이티브 타입 사용을 피하거나 제한하세요.
이 권장 사항은 라이브러리 API에서의 네이티브 타입 사용에만 적용되며, 애플리케이션 코드와는 무관해요. 라이브러리 구현에도 적용되지 않습니다. 예를 들면:
// Be extra careful! Native types are used in the library API:
public fun createUIView(): UIView
public fun handleThirdPartyNativeType(c: ThirdPartyNativeType)
// Be careful as usual; native types are not used in the library API:
internal fun createUIViewController(): UIViewController
public fun getDate(): String = NSDate().toString()
서드파티 라이브러리를 사용하는 라이브러리 게시하기
서드파티 네이티브 라이브러리를 사용하는 Kotlin 라이브러리를 게시한다면, 호환성 문제를 피하기 위해 할 수 있는 몇 가지가 있어요.
커스텀 패키지 이름 사용하기
서드파티 네이티브 라이브러리에 커스텀 패키지 이름을 사용하면 호환성 문제를 막는 데 도움이 될 수 있어요.
네이티브 라이브러리를 Kotlin에 가져오면 Kotlin 패키지 이름을 얻습니다. 고유하지 않으면 라이브러리 사용자가 충돌을 겪을 수 있어요. 예를 들어 사용자 프로젝트의 다른 곳이나 다른 의존성에서 네이티브 라이브러리를 같은 패키지 이름으로 가져오면 그 두 사용이 충돌합니다.
이런 경우 컴파일이 Linking globals named '...': symbol multiply defined! 오류로 실패할 수 있어요. 다만 다른 오류가 생기거나 컴파일이 성공할 수도 있습니다.
서드파티 네이티브 라이브러리에 커스텀 이름을 사용하려면:
- CocoaPods 통합으로 네이티브 라이브러리를 가져올 때는 Gradle 빌드 스크립트의
pod {}블록에서 packageName(pod 함수) 프로퍼티를 사용하세요. cinterops구성으로 네이티브 라이브러리를 가져올 때는 구성 블록에서 packageName(cinterops) 프로퍼티를 사용하세요.
이전 Kotlin 버전과의 호환성 확인하기
Kotlin 라이브러리를 게시할 때 서드파티 네이티브 라이브러리 사용은 다른 Kotlin 버전과의 호환성에 영향을 줄 수 있습니다. 구체적으로:
- Kotlin Multiplatform 라이브러리는 순방향 호환성(이전 컴파일러가 새 컴파일러로 컴파일된 라이브러리를 사용하는 것)을 보장하지 않아요. 실제로는 일부 경우 동작하지만, 네이티브 라이브러리를 사용하면 순방향 호환성이 더 제한될 수 있습니다.
- Kotlin Multiplatform 라이브러리는 역방향 호환성(새 컴파일러가 이전 버전으로 만들어진 라이브러리를 사용하는 것)을 제공합니다. Kotlin 라이브러리에서 네이티브 라이브러리를 사용하는 것은 보통 역방향 호환성에 영향을 주지 않아야 해요. 다만 호환성에 영향을 주는 컴파일러 버그가 있을 가능성은 커집니다.
정적 라이브러리 임베딩 피하기
네이티브 라이브러리를 가져올 때 -staticLibrary 컴파일러 옵션이나 .def 파일의 staticLibraries 프로퍼티(정적 라이브러리 포함)로 관련 정적 라이브러리(.a 파일)를 포함할 수 있어요. 그러면 라이브러리 사용자가 네이티브 의존성과 링커 옵션을 다룰 필요가 없습니다.
하지만 포함된 정적 라이브러리 사용을 어떤 식으로도 구성할 수 없어요. 배제할 수도, 교체(대체)할 수도 없죠. 그래서 사용자는 같은 정적 라이브러리를 포함하는 다른 Kotlin 라이브러리와의 잠재적 충돌을 해결하거나 그 버전을 조정할 수 없게 됩니다.
네이티브 라이브러리 지원의 진화
현재 Kotlin 프로젝트에서 C와 Objective-C를 사용하면 호환성 문제가 생길 수 있으며, 그중 일부가 이 가이드에 나열되어 있어요. 이들을 고치려면 향후 몇 가지 파괴적 변경이 필요할 수 있는데, 이 자체가 호환성 문제에 기여합니다.
Swift 라이브러리 가져오기
Kotlin/Native는 순수 Swift 라이브러리의 직접 가져오기를 지원하지 않아요. 하지만 이를 우회할 몇 가지 옵션이 있습니다.
한 가지 방법은 수동 Objective-C 브리징입니다. 이 방식을 쓰면 커스텀 Objective-C 래퍼와 .def 파일을 작성하고 cinterop을 통해 그 래퍼를 소비해야 해요.
하지만 대부분의 경우 역방향 가져오기(reverse import) 접근을 권장합니다. Kotlin 쪽에서 기대 동작을 정의하고, Swift 쪽에서 실제 기능을 구현한 뒤 Kotlin으로 다시 전달하는 방식이에요.
기대 부분은 다음과 같은 방식 중 하나로 정의할 수 있습니다:
- 인터페이스 만들기: 인터페이스 기반 접근은 여러 함수와 테스트 가능성 측면에서 확장성이 좋습니다.
- Swift 클로저 사용하기: 빠른 프로토타입에 훌륭하지만 한계가 있어요. 예를 들어 상태를 유지하지 않습니다.
- Swift export 사용하기(native-swift-export): Objective-C 브리징 없이 Kotlin 인터페이스를 Swift에서 직접 구현하고 Swift 객체를 Kotlin으로 다시 전달할 수 있어요.
CryptoKit Swift 라이브러리를 Kotlin 프로젝트에 역방향으로 가져오는 예시를 살펴볼게요.
인터페이스 기반 접근:
// CryptoProvider.kt
interface CryptoProvider {
fun hashMD5(input: String): String
}
// App.kt
@Composable
fun App(cryptoProvider: CryptoProvider) {
// Example usage inside your UI
val hashed = cryptoProvider.hashMD5("Hello, world!")
androidx.compose.material3.Text("Compose: $hashed")
}
// MainViewController.kt
fun MainViewController(cryptoProvider: CryptoProvider) = ComposeUIViewController {
App(cryptoProvider)
}
// iosApp/ContentView.swift
import CryptoKit
class IosCryptoProvider: CryptoProvider {
func hashMD5(input: String) -> String {
guard let data = input.data(using: .utf8) else { return "failed" }
return Insecure.MD5.hash(data: data).description
}
}
// iosApp/ContentView.swift
struct ComposeView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
// Inject the Swift implementation into the Kotlin UI entry point
MainViewControllerKt.MainViewController(cryptoProvider: IosCryptoProvider())
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}
클로저 기반 접근:
// App.kt
@Composable
fun App(md5Hasher: (String) -> String) {
// Example usage inside your UI
val hashed = md5Hasher("Hello, world!")
androidx.compose.material3.Text("Compose: $hashed")
}
// MainViewController.kt
fun MainViewController(md5Hasher: (String) -> String) = ComposeUIViewController {
App(md5Hasher)
}
// iosApp/ContentView.swift
import CryptoKit
import SwiftUI
struct ComposeView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
MainViewControllerKt.MainViewController(md5Hasher: { input in
guard let data = input.data(using: .utf8) else { return "failed" }
return Insecure.MD5.hash(data: data).description
})
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}
Swift export 접근:
// CryptoProvider.kt
interface CryptoProvider {
fun hashMD5(input: String): String
}
fun processHash(provider: CryptoProvider, input: String): String = provider.hashMD5(input)
open class SwiftBase
// iosApp/ContentView.swift
import CryptoKit
final class IosCryptoProvider: SwiftBase, CryptoProvider {
func hashMD5(input: String) -> String {
guard let data = input.data(using: .utf8) else { return "failed" }
return Insecure.MD5.hash(data: data).description
}
}
let provider = IosCryptoProvider()
// Calls the Kotlin function, which calls hashMD5() back in Swift
print(processHash(provider: provider, input: "Hello, world!"))
더 복잡한 프로젝트에서는 의존성 주입을 사용해 Swift 구현을 Kotlin으로 전달하는 것이 더 편리합니다. 자세한 내용은 의존성 주입 프레임워크 문서나 Koin 프레임워크 문서를 참고하세요.