KMM 을 통한 Android, iOS 간 코드 공유

Part 1 — KMP 와 KMM 이란 무엇인가?

Myungpyo Shim
8 min readJan 26, 2021

목차로 돌아가기 >

먼저 KMP 와 KMM 에 대해서 알아 보도록 하겠습니다.
KMP(Kotlin MultiPlatform) 는 멀티 플랫폼을 타겟으로하는 프로젝트를 수행할 때 Kotlin 을 이용하여 공통 모듈을 개발하고 이를 각각의 플랫폼에서 공유하여 개발할 수 있도록 지원해 줍니다. KMM(Kotlin Multiplatform Mobile) 은 KMP 에서도 특히 모바일 플랫폼(Android/iOS) 간 코드 공유를 위해 특화된 서브셋 이라고 볼 수 있습니다.

먼저 KMP 에서 Kotlin 으로 작성되는 공통 모듈은 다음과 같이 다양한 플랫폼을 타겟으로 빌드될 수 있습니다.

가장 가운데 Common Kotlin(보라색) 은 모든 플랫폼이 공유하는 공통 로직이며 순수 Kotlin 으로 구현합니다. 순수 Kotlin 이란 특정 플랫폼 의존성 없이 Kotlin 만으로 구현된 다는 것을 의미합니다.

그 다음 레이어인 Kotlin/X(초록색) 는 Kotlin 으로 구현하지만 플랫폼 지원을 위해 제공되는 Kotlin/X 라이브러리들을 이용하여 플랫폼 의존적인 부분에도 접근할 수 있습니다. 그러면 각각의 Kotlin/X 에 대해서 간략히 알아봅시다.

  • Kotlin/JVM 은 JVM 상에서 동작하는 Java Application 을 타겟으로 하며 Android 가 사용하는 DVM(DalvikVM) 역시 JVM 의 서브셋 이므로 이 타겟을 사용합니다. Java 기반의 Android 프로젝트를 수행해 본 개발자라면 Java 에서 Kotlin 으로 프로젝트를 전환하는 동안 Java <-> Kotlin 간 상호 호출을 위한 여러가지 규칙들이 있다는 것을 알고 있을 것 입니다. 이러한 규칙들은 Kotlin/JVM 타겟에서도 동일하게 적용됩니다.
    (i.e. Kotlin -> Java, Java -> Kotlin)
  • Kotlin/JS 는 Web - Frontend / Backend 를 위한 Javascript 기반 플랫폼을 타겟으로 하며 Kotlin 을 이용하여 안전한 타입 시스템과 비교적 적은 메모리 사용량 등의 장점들을 제공합니다. Kotlin/JS 를 이용하여 생성 된 Javascript 코드는 이 글을 작성하는 현재 ES5 를 타겟으로 하고 있으며 Kotlin/JS IR 컴파일러는 d.ts (Typescript declaration files) 을 생성하여 Kotlin+TypeScript 애플리케이션의 빌드를 수월하게 합니다.
  • Kotlin/Native 는 가상 머신이 적합하지 않거나 동작 불가능 한 플랫폼을 타겟으로 하며 각 플랫폼에 맞는 바이너리를 빌드합니다. 예를들면 iOS/Android 용 arm32/64 나 Desktop 용 x86_64 등을 타겟으로 하는 C/C++ 라이브러리를 빌드 할 수 있고 그에 필요한 헤더 파일들도 함께 생성됩니다. 또한 iOS 에서 사용되는 framework 형식으로도 빌드 할 수 있습니다. Kotlin/Native 에는 수많은 유용한 라이브러리들(Foundation, OpenGL, gzip, …)이 컴파일러 패키지로 기본 제공되고 있기 때문에 네이티브 플랫폼 간 코드 공유를 수월하게 해줍니다.

위와같이 KMP 를 이용하면 Kotlin 으로 공통 코드를 작성하여 다양한 플랫폼을 타겟으로 빌드할 수 있기 때문에 멀티 플랫폼을 지원하는 프로젝트 진행 시 공통 모듈 작성 후 각각의 플랫폼 특화된 모듈이 이 공통 모듈을 이용하여 각 플랫폼 타겟에 맞게 빌드되도록 할 수 있습니다.

예를들어 위와 같이 JVM / JS / Desktop 플랫폼을 지원하는 프로젝트 진행 시 Kotlin 으로 공통 모듈을 작성합니다. 이 때, 모든 플랫폼이 공유하는 코드는commonMain 이라는 순수 Kotlin 모듈로 만들고, 각 플랫폼에 특화 된 코드는 각 플랫폼 모듈(ex> jvmMain, jsMain)에 구현합니다. 예를들어 commonMain 에서 사용 해야하는 어떤 기능이 각 플랫폼의 구현에 의존적이어서 플랫폼 마다 다르게 구현되어야 한다면 KMP 의 expect-actual 매커니즘을 이용하여 기능의 정의부와 구현부를 분리하고, 정의는 commonMain 에 하고 구현은 각 플랫폼 모듈에 구현되도록 할 수 있으며 이에 대해서는 잠시 뒤에 다시 다루겠습니다.

전체적인 프로젝트 관점에서 보면 위 이미지 전체가 공통 모듈이며 commonMain 은 순수 Kotlin 으로 구현된 공통 모듈 코어 라고 할 수 있습니다.

또한 위 그림에서desktopMain 은 데스크탑 타겟에서 각각의 세부적인 하위 모듈을 갖고 있으며 이들의 공통 로직을 갖는 모듈입니다. 만약 데스크탑 타겟에 공통으로 사용할 만한 부분이 없다면 desktopMain 으로 추상화 하지 않고 commonMain 모듈 하위에 각 모듈 5개가 바로 위치하도록 만들 수도 있습니다.

KMM 은 위와 같은 KMP 개발 모델에서 모바일에 특화된 개발 모델입니다. 현재로서는 사실상 Android / iOS 가 모바일 OS 를 대표하고 있으므로 이 두 플랫폼 간 공통 로직을 공유하기 위한 개발 모델이라고 볼 수 있습니다. 그래서 공통 모듈(shared)에서 commonMain 패키지에는 Android / iOS 에 공통으로 사용 될 비지니스 로직들이 위치하고 각 플랫폼에 특화된 로직들은 androidMain, iosMain 패키지에 각각 위치하게 됩니다. androidMain 패키지에서는 Android Framework 에 접근 가능하며, iosMain 패키지에서는 Foundation 이나 UiKit 같은 iOS Framework 에 접근 가능합니다.

KMM 을 맛보기 위해 다음 파트부터 작성 해 볼 샘플 프로젝트도 KMM 을 이용하여 Android / iOS 모듈이 공통으로 갖는 비지니스 로직을 공통 모듈화하고 이 공통 모듈을 각 플랫폼에 맞게 빌드하여 각 플랫폼에서 사용하도록 구현합니다.

아래 이미지는 KMM 프로젝트의 기본적인 구조를 나타내며 우리가 만들 샘플 프로젝트의 각 모듈 디렉토리에 매핑하여 나타내 본 것 입니다.

샘플 프로젝트의 자세한 내용은 본격적으로 프로젝트를 만들면서 다루겠지만 KMM 에 대한 이해를 조금 더 돕기 위해 간략히 살펴보겠습니다.

왼쪽 이미지를 보면 아래쪽 레이어가 공통 모듈(Shard Code) 이고 그 위에 각 플랫폼 애플리케이션 들이 있습니다. 공통 모듈 레이어는 앞서 이야기 했던 것처럼 Kotlin 으로 작성되며 빌드 시 각 타겟 플랫폼에서 이용 가능한 아카이빙 형식으로 빌드됩니다. 공통 모듈은 다음과 같이 구성됩니다.

  • commonMain : 각 플랫폼이 순수하게 공유하는 로직이 담긴 부분으로 순수 Kotlin 으로 작성됩니다.
  • androidMain : Android 플랫폼에 특화된 로직이 담기는 부분으로 Android 프레임워크에 접근할 수 있습니다.
    (android.os.X, Managers, SqliteOpenHelper…)
  • iosMain : iOS 플랫폼에 특화된 로직이 담기는 부분 부분으로 iOS 프레임워크에 접근할 수 있습니다.
    (Foundation, UIKit, CoreData…)

특히 공통 모듈에서도 commonMain 에는 모든 플랫폼이 공유하는 비지니스 로직이 순수 코틀린 만으로 작성되는데 어찌보면 당연하게도 이곳에 각 플랫폼에 특화된 구현이 필요한 경우가 발생합니다. 이 때, KMP(또는 KMM) 프로젝트에서는 플랫폼에 의존적인 기능을 추상화하여 expect 라는 키워드를 이용하여 공통 영역인 commonMain에 정의하고, 각 플랫폼 프레임워크에 접근하여 플랫폼 의존적인 기능을 구현할 수 있는 영역(androidMain, iosMain)에서 actual 이라는 키워드를 통해 구현합니다. 이를 통해 공통 영역(commonMain)에서는 각 플랫폼 구현체에 신경쓰지 않고 expect 가 붙은 추상화 된 인터페이스를 호출하여 공통 비지니스 로직을 구현 할 수 있게 됩니다.

이러한 expect-actual 매커니즘은 다음과 같은 규칙을 갖습니다.

  • 정의부에 expect 키워드를 사용하고, 구현부에 actual 키워드를 사용
  • expect 와 actual 정의부+구현부 쌍은 동일한 이름과 패키지명을 가져야 함
  • expect 를 이용한 정의부는 구현부를 가질 수 없음

샘플 프로젝트에서는 가장 기본적인 expect-actual 매커니즘의 적용 예로 각 플랫폼에 적절한 로깅을 위해 commonMain 에 로거 인터페이스를 expect 로 정의하고 각 플랫폼 모듈에서 actual 로 구현하는 것을 볼 수 있습니다.

다음 (KMM 으로 샘플 프로젝트 개발 준비) >
목차로 돌아가기 >

--

--