Swift export를 통한 Swift 상호 운용성
Swift export를 통한 Swift 상호 운용성
Swift export를 통한 코틀린의 Swift 상호 운용성은 현재 Alpha 단계예요. Swift export는 코틀린 소스를 직접 export해서, Objective-C 헤더 없이도 Swift에서 코틀린 코드를 관용적으로 호출할 수 있게 해 줘요.
본문
Swift export는 Apple 타깃의 멀티플랫폼 개발을 더 매끄럽게 만들어요. 예를 들어 최상위 함수가 있는 코틀린 모듈이 있다면, Swift export는 깔끔하고 모듈별로 특화된 import를 제공해서 혼란스러운 Objective-C 밑줄과 맹글링된 이름을 제거해 줘요.
현재 Swift export의 기능은 다음과 같아요.
- 다중 모듈 지원 — 각 코틀린 모듈이 별도의 Swift 모듈로 export되어 함수 호출을 단순화해요.
- 패키지 지원 — 코틀린 패키지가 export 중 명시적으로 보존되어 생성된 Swift 코드에서 이름 충돌을 피해요.
- 타입 별칭 — 코틀린 타입 별칭이 export되어 Swift에 보존되어 가독성을 높여요.
- 원시 타입의 향상된 널 가능성 — 널 가능성을 보존하려고
Int?같은 타입을KotlinInt같은 래퍼 클래스로 박싱해야 했던 Objective-C interop과 달리, Swift export는 널 가능성 정보를 직접 변환해요. - 오버로드 — 모호함 없이 Swift에서 코틀린의 오버로드된 함수를 호출할 수 있어요.
- 평평한 패키지 구조 — 코틀린 패키지를 Swift 열거형으로 변환해 생성된 Swift 코드에서 패키지 접두사를 제거할 수 있어요.
- 모듈 이름 커스터마이징 — 코틀린 프로젝트의 Gradle 구성에서 생성되는 Swift 모듈 이름을 커스터마이징할 수 있어요.
- 동시성 지원 — Swift에서 일시 중단 코틀린 코드를 매끄럽게 호출하고, kotlinx.coroutines 플로우를 기본 제공되는 Swift
AsyncSequence로 export할 수 있어요.
Swift export 활성화
Swift export는 현재 Alpha 단계이고 아직 불완전해서, 파괴적 변경이 예상돼요. 시도해 보려면 코틀린 프로젝트의 빌드 파일을 구성하고, Swift export를 통합하도록 Xcode를 설정하세요.
코틀린 프로젝트 구성
다음 빌드 파일을 Swift export 설정의 시작점으로 사용할 수 있어요.
// build.gradle.kts
kotlin {
iosArm64()
iosSimulatorArm64()
swiftExport {
// Set the root module name
moduleName = "Shared"
// Set the collapse rule
// Removes package prefix from generated Swift code
flattenPackage = "com.example.sandbox"
// Configure external modules export
export(project(":subproject")) {
// Set the name for the exported module
moduleName = "Subproject"
// Set the collapse rule for the exported dependency
flattenPackage = "com.subproject.library"
}
// Provide compiler arguments to link tasks
configure {
freeCompilerArgs.add("-Xexpect-actual-classes")
}
}
}
코틀린 컴파일러는 필요한 모든 파일(swiftmodule 파일, 정적 .a 라이브러리, 헤더 파일, modulemap 파일 포함)을 자동으로 생성해서 앱의 빌드 디렉터리로 복사해요. 이 디렉터리는 Xcode에서 접근할 수 있어요.
Xcode 프로젝트 구성
Xcode를 구성해 Swift export를 프로젝트에 통합하는 방법은 다음과 같아요.
- Xcode에서 프로젝트 설정을 열어요.
- Build Phases 탭에서
embedAndSignAppleFrameworkForXcode작업이 있는 Run Script 단계를 찾아요. - 그 스크립트를 run script 단계의
embedSwiftExportForXcode작업으로 바꿔요.
[이미지: Swift export 스크립트 추가]./gradlew :<Shared module name>:embedSwiftExportForXcode - 프로젝트를 빌드해요. 빌드는 출력 디렉터리에 Swift 모듈을 생성해요.
현재 제한 사항
Swift export는 현재 iOS 프레임워크를 Xcode 프로젝트에 연결하는 직접 통합(direct integration) 방식을 쓰는 프로젝트에서만 동작해요. 이는 IntelliJ IDEA의 Kotlin Multiplatform 플러그인이나 웹 마법사로 만든 Kotlin Multiplatform 프로젝트의 표준 구성이에요.
그 외 알려진 문제는 다음과 같아요.
List,Set,Map을 상속하는 타입은 export 중 무시돼요 (KT-80416).List,Set,Map의 하위 클래스는 Swift 쪽에서 인스턴스화할 수 없어요 (KT-80417).- Swift로 export될 때 코틀린 제네릭 타입 파라미터는 상한(upper bound)으로 타입 소거(type-erasure)돼요.
- IDE 마이그레이션 팁이나 자동화는 없어요.
- opt-in이 필요한 선언을 사용할 때는 Gradle 빌드 파일의 모듈 레벨에 명시적
optIn컴파일러 옵션을 추가해야 해요. 예를 들어 kotlinx.datetime 라이브러리의 경우:swiftExport { moduleName = "Shared" export("org.jetbrains.kotlinx:kotlinx-datetime:0.8.0") { moduleName = "KotlinDateTime" flattenPackage = "kotlinx.datetime" } } // Add a separate opt-in block at the module level compilerOptions { optIn.add("kotlin.time.ExperimentalTime") }
매핑(Mappings)
아래 표는 코틀린 개념이 Swift로 어떻게 매핑되는지 보여 줘요.
| Kotlin | Swift |
|---|---|
| class | class |
| object | class with shared property |
| enum class | enum |
| sealed classes and interfaces | enum |
| typealias | typealias |
| Function | Function |
| suspend fun | async |
| kotlinx.coroutines flows | AsyncSequence |
| Property | Property |
| Constructor | Initializer |
| Package | Nested enum |
| Boolean | Bool |
| Char | Unicode.UTF16.CodeUnit |
| Byte | Int8 |
| Short | Int16 |
| Int | Int32 |
| Long | Int64 |
| UByte | UInt8 |
| UShort | UInt16 |
| UInt | UInt32 |
| ULong | UInt64 |
| Float | Float |
| Double | Double |
| Any | KotlinBase class |
| Unit | Void |
| Nothing | Never |
선언
클래스
Swift export는 class Foo()처럼 Any에서 직접 상속하는 final 클래스만 지원해요. 이런 클래스는 특별한 KotlinBase 클래스를 상속하는 Swift 클래스로 번역돼요.
Object
Object는 private init과 정적 shared 접근자를 가진 Swift 클래스로 번역돼요.
타입 별칭
코틀린 타입 별칭은 있는 그대로 export돼요.
열거형
코틀린 enum class 선언은 일반 네이티브 Swift enum 타입으로 export돼요.
Sealed 클래스와 인터페이스
코틀린에 정의된 sealed 계층은 Swift 열거형으로 매핑되어 완전한(exhaustive) switch 문을 가능하게 해 줘요.
Swift export는 각 sealed 타입에 .sealedType() 메서드를 생성해요. 이 메서드는 sealed 계층의 직접 하위 클래스에 맞는 case를 가진 Swift 열거형을 반환해요. 이 호출을 중첩하면 계층의 더 깊은 수준에 맞출 수 있어요.
예를 들어 코틀린에서 sealed 인터페이스와 클래스 계층을 선언해 보세요.
// Kotlin
sealed interface Shape
class Circle : Shape {
override fun toString(): String = "Circle"
}
class Rectangle : Shape {
override fun toString(): String = "Rectangle"
}
fun createCircle(): Shape = Circle()
Swift 쪽에서는 default case 없이 완전한 switch를 사용할 수 있어요.
// Swift
let shape = createCircle()
let name = switch shape.sealedType() {
case let .circle(type): "It's a \(type.value)"
case let .rectangle(type): "It's a \(type.value)"
}
// name == "It's a Circle"
switch가 완전하기 때문에 sealed 계층에 새 하위 클래스가 추가되면 컴파일러가 경고를 띄워요. 그래서 switch의 default case에 의존하는 대신 즉시 처리할 수 있죠.
함수
Swift export는 간단한 최상위 함수와 메서드를 지원해요.
// Kotlin
fun foo(a: Short, b: Bar) {}
fun baz(): Long = 0
// Swift
public func foo(a: Swift.Int16, b: Bar) -> Swift.Void {
// ...
}
public func baz() -> Swift.Int64 {
// ...
}
코틀린 확장 함수의 경우 리시버 파라미터가 첫 번째 위치의 일반 Swift 파라미터가 돼요.
// Kotlin
fun Int.foo(): Unit = TODO()
// Swift
func foo(_ receiver: Int32) {}
vararg가 있는 코틀린 함수는 Swift의 가변(variadic) 함수 파라미터로 매핑돼요.
프로퍼티
코틀린 프로퍼티는 Swift 프로퍼티로 번역돼요.
생성자
생성자는 Swift 초기화자로 번역돼요.
타입
kotlin.Nothing
코틀린 Nothing 타입은 Never 타입으로 번역돼요.
클래스 분류 타입
Swift export는 현재 Any에서 직접 상속하는 final 클래스만 지원해요.
패키지
코틀린 패키지는 이름 충돌을 피하기 위해 중첩된 Swift 열거형으로 번역돼요.
동시성
일시 중단 함수
Swift에서 일시 중단 코틀린 코드를 호출할 수 있어요. 코틀린 일시 중단 함수와 일시 중단 함수 타입은 Swift의 async 대응물로 export돼요.
플로우
kotlinx.coroutines 플로우를 Swift의 AsyncSequence로 export할 수도 있어요.
코루틴 디스패처
기본적으로 Swift에서 코틀린 일시 중단 함수를 호출하거나 asAsyncSequence 함수를 사용하면, 코틀린은 Dispatchers.Default 디스패처를 사용하는 코루틴 컨텍스트를 만들어 export된 코드를 그곳에서 실행해요.
export된 코드를 다른 디스패처에서 실행하려면 코틀린에서 withContext() 함수로 코루틴 컨텍스트를 바꾸세요. 예를 들어:
교차 언어 상속
Swift export는 교차 언어 상속(cross-language inheritance)을 지원해요. 이 기능의 일반적인 사용 사례는 **역방향 import 패턴(reverse import pattern)**으로, 코틀린에서 계약(contract)을 정의하고 Swift 쪽에서 플랫폼별 구현을 제공하는 방식이에요. 코틀린으로 직접 import할 수 없는 순수 Swift 라이브러리를 써야 할 때 특히 유용해요.
이 패턴을 구현하려면 Swift 구현이 상속할 수 있는 코틀린 인터페이스와 코틀린 슈퍼클래스를 선언해야 해요. 그런 다음 Swift에서 인터페이스를 구현하고, 그 인터페이스를 받아들이는 코틀린 함수에 Swift 객체를 전달해요. 예를 들어 CryptoKit 라이브러리의 경우:
- 코틀린 쪽에서 인터페이스, 그것을 받는 함수, open 기본 클래스를 선언해요.
// Kotlin interface CryptoProvider { fun hashMD5(input: String): String } fun processHash(provider: CryptoProvider, input: String): String = provider.hashMD5(input) open class SwiftBase - Swift 쪽에서 export된
SwiftBase클래스를 상속하고, 순수 Swift 라이브러리로 인터페이스를 구현한 뒤 객체를 코틀린에 다시 전달해요.// 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 객체를 받으면, 그 객체를 일반 코틀린 인터페이스의 구현처럼 취급해서 Swift 코드를 직접 호출해요.
Swift export의 진화
우리는 향후 코틀린 릴리스에서 Swift export를 확장하고 점진적으로 안정화해서, 코틀린과 Swift 사이의 상호 운용성을 개선할 계획이에요. 피드백을 남길 수 있어요.
- Kotlin Slack에서 초대를 받아 #swift-export 채널에 참여하세요.
- YouTrack에서 이슈를 보고하세요.
더 알아보기
- Swift/Objective-C와의 상호 운용성
- Kotlin/Native 개요
- Kotlin Multiplatform 개요