2022년 9월 11일 일요일

Kotlin Multiplatform

Kotlin Multiplatform

KMP , KMM

Kotlin Multi Platform (Mobile) 의 줄임말이다.
Kotlin으로 Android,iOS, Jvm(javaFx or Spring App), Javascript, Desktop(with jet composer) 프로젝트 만들수 있다.
Kotlin으로 저걸 다 만들자가 아니라, Kotlin으로 도메인의 비지니스로직을 만들고 각각의 플랫폼에서 공통으로 사용할수 있는 환경을 만들어 주자는 거다.
각 플랫폼은 화면단의 처리만 한다고 생각하면 된다. 각 플랫폼만의 특징이 있기떄문에 그걸 하나의 코드로 퉁치는것은 힘들고 또한 각각의 네이티브한 표시기법이 있기때문에 그것은 그대로 살려두고, 그 안쪽의 것만 같은 것을 사용할수 있도록 해준다는 개념이다.
따라서 플러터나 리액티브 처럼 하나의 코드로 여기저기 실행됩니다 라는 개념은 아니라 common모듈(이름은 개발자맘대로해도된다.)을 각 플랫폼에서 동일한 방식으로 호출하는것에 개발초점을 두면된다.

프로젝트 시작/설정

IntelliJ IDEA 에서 새프로젝트를 Kotlin-> Multiplatform 항목에서 선택하여 생성하면된다. Mobile Application을 선택하면 된다.
일단 생성하고 나중에 JVM, Javascript등을 수동으로 추가하면 된다.

1. setttings.gradle.kts

프로젝트를 생성하고 Project 탭의 화면에 보면 settings.gradle.kts 파일이 있다.
이 화일에서는 빌드 전체에 사용하는 (그래이들용)플러그인들에 영향을 주는 설정을 할수 있다.

  • 각 서브 프로젝트나 모듈들은 플러그인 항목을 통해서 사용할 플러그인을 지정하는데 이때 플러그인을 찾을 저장소와 버젼등을 지정할수 있도록 해준다.
    setting.gradle.kts
pluginManagement {  
  repositories {  
    여기선 레포지트리 gogole(), mavenCenter() 등을 지정한다.
    별도의 저장소가 있다면 해당 저장소를 등록하면된다. 
  }  

  resolutionStrategy {  
    여러개의 플러그인이 서로 다른 버젼으로 모듈마다 지정되어있을때 이곳에서 하나의 버젼으로 
    eachPlugin {  
      여기선 읽어들인 플러그인에 대해서 사용할 모듈이나 버젼들을 지정할수 있다. 
      기본적으로 프로젝트에서 id("플러그인ID")를 입력하면 저장소에서 해당 id의 플러그인을 최선버젼으로 가져온다.
      예를 들어 kotlin2js라는 플러그인을 호출할때 "org.jetbrains.kotlin:kotlin-gradle-plugin" 모듈을 호출하고 싶을때는
      if (requested.id.id == "kotlin2js") {  
          useModule("org.jetbrains.kotlin:kotlin-gradle-plugin:${requested.version}")  
          useVersion("1")
      }       
     라고 쓰면된다. 
     버젼도 지정할수 있다.

        }
     }  
  }  
// 하위 프로젝트나 모듈들은 build.gradle.kts 파일에서 아래처럼 플러그인을 지정해서 사용한다.
plugins {
    id("com.mycompany.sample")
    kotlin("js")
}

예를들어 각각의 모듈에서 Serializeation 플러그인을 사용할때,

  1. js-app : kotlin(“plugin.serialization”) version “1.3.0”
  2. androd-app:kotlin(“plugin.serialization”) (verstion을 지정안하면 최신버젼을 가져온다.)

처럼 각각의 모듈들이 서로다른 버젼의 사용으로 인해 문제가 발생하거나 최종화일이 쓸데없이 커질때, 가능하면 하나의 버젼으로 통일시키고 싶을때는 각각의 모듈설정파일 gradle.build.kts 에서 하는게 아니라 setting.gralde.kts 에서 한번에 지정하면된다.
if (requested.id.id == “org.jetbrains.kotlin.plugin.serialization”) {
useVersion(“1.5.31”)
}
위 처럼 하면 빌드시에 org.jetbrains.kotlin.plugin.serialization id를 가진 플러그인 요청에 대해 무조건 1.5.31 을 사용하도록 한다.
이렇게 resolutionStragy(해결전략) 을 짤수 있도록 해준다.

이방식으로는 classpath 라는것으로 해당 플러그인의 이름과 버젼을 classpath(“그룹:이름:버젼”) 식으로 지정했는데, resolutionStragy 를 이용하면 classpath를 사용하지 않고 useModule을 통해서 지정할수 있다.

if (requested.id.id == “com.android.application”) {
useModule(“com.android.tools.build:gradle:4.2.2”)
또는
useVersion(“4.2.2”)
}
이렇게 지정하면 하위의 build.gradle.kts 에서는 classpath(“com.android.tools.build:gradle:4.2.2”) 를 지정할 필요가없다.

  • settings.gradle.kts는 사용할 모듈을 include로 지정한다.
    레포지토리를 따로 관리하기 위해서는 project(":androidApp").projectDir = file("…/androidApp") 처럼할수도있다.

2.build.gradle.kts

작업중인 프로젝트 전체에 영향을 주는 빌드스크립트이다.
주로 각 하위모듈에서 참고할 저장소와 의존성(사용할 라이브러리가 어디서오고 어떤 버젼을 사용하는지)을 지정할수 있다.
같은 라이브러리여도 버젼에 따라서 사용법과 기능이 차이가 있기때문에 의존관계를 지정하여 주는거다.

group : 외부참조용 프로젝트 그룹명을 지정한다. 라이브러리나 플러그인을 불러올때 ( "그룹명:이름:버젼") 으로 참조하여 가져오는데 그때 사용할 그룹명을 지정한다. 
version: 위에서 말한 버젼이다. 
allprojects {
  1. by extra 로 변수 사용
     아래처럼 by extra 를 사용하면 하위 gradle에서 [val momo: Map<String, String> by extra] 처럼 선언하여 프로젝트전역에 걸친 변수를 사용할수 있다.
  val momo by extra {
    mapOf(  
       "springBoot" to "2.2.0.M6"  
   )
  }
  2. task.create(register) 
     프로젝트의 모든 모듈에 동일한 작업을 수행하는 task를 생성해준다. 
  3. repositories
     하위 모듈들이 빌드시에 참조할 저장소를 지정한다.
  4. configurations.all 
     이곳에서는 resolutionStrategy 을 이용해서 라이브러리 모듈을 강제로 특정 모듈 또는 버젼으로 지정한다든지, 다른 모듈로 대치하는 등의 처리를 할 수있다.
     https://star-zero.medium.com/gradle%E3%81%AEresolutionstrategy-dc948fb05b54 참고
  5. group, version
     이곳에 그룹과 버젼을 입력하면 전체 프로젝트가 동일한 그룹과버젼으로 된다.
     
} 

subprojects {
allprojects와 설정은 동일하다. 최상위프로젝트와 하위 프로젝트들을 다르게 설정할경우 이 블록을 사용한다.
}

project(":api명"){
  dependencies{}  특정 프로젝트가 실행되기 전에 먼저 빌드가 이루어져야 할경우 사용한다.
}

하위프로젝트

KMM으로 프로젝트를 생성했다면 기본적으로 android-app , iosApp, Shared 라는 하위프로젝트(모듈)이 생성되어 진다.
kotlin 멀티플랫폼에서는 이렇게 하위 프로젝트로서 각각의 플랫폼에 해당하는 폴더를 생성하고, shared 라이브러리에서 작성한 공통코드를 각각의 플랫폼에서 불러다 사용할수 있도록 해주는 구조이다.
따라서 각각의 플랫폼폴더의 구조는 각 애플리케이션의 작업폴더구조대로 작업하면된다.
플랫폼을 추가할수도 있는데, 루트프로젝트폴더에 해당플랫폼의 폴더를 만들고 그안에 build.gradle.kts 파일을 만들면된다.
만들고 나서는 프로젝트의 루트 settings.gradle.kts에 include(":프로젝트명") 을 입력하여 모듈을 추가해야한다.
그리고 shared/src의 하위 폴더를 생성하여 공통처리될 코드를 입력하면 된다.

Shared폴더에 구현하는 코드는 크게 3가지 형태로 나뉜다.

  1. 플래폼에 의존하지 않는 데이터 처리. kotlin 멀티플랫폼에서 기본적으로 지원하는 기능들을 이용해서 구현할 경우 프로젝트에서 shared라이브러리를 참조 하는 것만으로도 불러와 사용할수 있다.
  2. 플랫폼에 의존하는 고유한 처리. 각각의 플래폼에서 데이터를 얻을수 밖에 없는 고유한 처리의 경우 예를 들어 모바일의 경우는 단말의 정보등은 shared 모듈에서 알수가 없는 정보이기에, 에상(expect) 와 실체(actual) 키워드로 지정하여 작업한다. shared에서 예상한 기능이 해당 플래폼에서의 실체는 이렇게 동작한다는 형태로 구현하여 각 플랫폼에 적절한 처리가 되도록 한다.
  3. 플랫폼에 의존하지만 공통으로 사용할 수 있도록 라이브러리를 사용한처리. kotlin 멀티플랫폼용 라이브러리가 있어서 여러플랫폼에서 동일하게 구현될수 있도록 해준다. 이때는 expect와 actual 없이 작업해도 된다. 예를 들어 시간,네트워크 같은 라이브러리가 있다.

서로다른 플랫폼에서 공통의 작업, 예를 들어 Android , iOS 각각의 플랫폼에서 네트워크로 부터 데이터를 요청하는 작업을 할때 각 플랫폼에서 동작할 예상(expect) 라는 키워드로

buildSrc

각 프로젝트마다 build.gradle.kts파일에 사용할라이브러리와 플러그인의 이름과 버젼을 직접입력하여 관리하는것은 매우 불편하다.
특히 KMM(or KMP)에서는 각기 다른 플랫폼별로 build.gradle.kts파일이 있어서 공통된 특정 라이브러리의 버젼이 바뀔때 마다 해당 하는것은 실수를 부르기 마련이다.
루트 프로젝트의 바로 하위로 buildSrc라는 폴더를 생성하고 모듈로 인식할수 있도록 build.gralde.kts 파일을 두고 , src/main/java/ 폴더를 만들어서 하위에 .kt 파일로 object 형태로 변수와 패키지명을 정의하여 다른 build.gradle.kts에서 해당 오브젝트 변수를 가져와서 설정할수 있다.

  1. KMM으로 멀티 플랫폼을 만들자(Android,iOS용)
  2. 프로젝트 루트폴더밑에 buildSrc 를 만들고 그 안에 build.gradle.kts를 만든다
repositories {  
  jcenter() 
  google()  
}  
  
plugins {  
  `kotlin-dsl`  
}
  1. src/main/java/Versions.kt 파일을 만들고, 버전정보를 입력한다.
Version.kt
object Versions {  
    // Kotlin  
  const val kotlin = "1.5.31"  
  
  // Android  
  const val androidMinSdk = 21  
  const val androidCompileSdk = 31  
  const val androidTargetSdk = androidCompileSdk
  
  // AndroidX 
  const val androidx = "1.0.0"
  const val androidNavigation = "2.3.0"
}

프로젝트에 따라서 AndroidDeps, SharedDeps, SpringBootDeps 등등을 추가하면된다.

AndroidDeps.kt 
object AndroidDeps {
	object Plugins {  
	    object Kotlin {  
	        val android = "android"  
			val kapt = "kapt"  
	  }  
	  
	    object Id {  
	        val androidApp = "com.android.application"  
		    val safeArgs = "androidx.navigation.safeargs.kotlin"  
	  }  
	}    
  
	object AndroidVersions {  
	  val minSdk = 24  
	  val targetSdk = 30  
	  val compileSdk = 30  
	}
	  
	object ClassPath {  
	    val safeArgsGradlePlugin =  
	        "androidx.navigation:navigation-safe-args-gradle-plugin:${Versions.androidNavigation}"  
	}
}
  1. 프로젝트의 build.gradle.kt 파일을 수정하자.
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31")classpath(Deps.ClassPath.kotlinGradlePlugin)로 고치자
    
classpath("com.android.tools.build:gradle:4.1.2")classpath(Versions.ClassPaths.androidGradle)로 고치자
  1. androidApp 프로젝트의 build.gradle.kt파일을 수정하자
kotlin("android")kotlin(AndroidDeps.Plugins.Kotlin.android)로 고치자
    
minSdkVersion(24)classpath(AndroidDeps.AndroidVersions.minSdk)로 고치자

이렇게 buildSrc폴더하위에 라이브러리, 플러그인들의 버젼과 이름을 명시하여 같은 라이브러리를 사용하는 여러 모듈에서 일관성있게 관리할수 있다.

shared 폴더

shared 폴더는 모든 플랫폼에서 공통으로 처리될 만한 (주로) 비지니스 로직(도메인)이 들어간다.
shared/build.gradle.kts 파일에는 각 플랫폼별 설정과 플랫폼별 사용하게될 소스코드의 위치를 지정한다.
각 플랫폼별 코드를 작성하기 위해서는 *Main 이라는 이름으로 폴더를 생성하고 sourceSets {} 에 폴더의 이름을 지정하면된다. KMM 프로젝트를 생성했다면 기본적으로 androidMain, iosMain 그리고 commonMain 폴더가 생성되어있고 sourceSet 에는 각각 지정되어 있다.

 
kotlin {  
     android()  
     ios {  
   	  binaries {  
   		  framework {  
   			  baseName = "shared"  
   		  }  
   	 } 
    }  
    sourceSets {  
     val commonMain by getting  
     val commonTest by getting {  
   	  dependencies {  
   			  implementation(kotlin("test"))  
          }  
      }  
      val androidMain by getting {  
         dependencies {  
   		      implementation("com.google.android.material:material:1.2.1")  
        }  
      }  
      val androidTest by getting {  
         dependencies {  
           implementation("junit:junit:4.13")  
          }  
        }  
      val iosMain by getting  
      val iosTest by getting  
   }  
}

shared/gradle.build.kts 파일은 안드로이드 라이브러리 형태로 안드로이드앱에 포함되기 때문에, plugin 에 com.android.library 플러그인이 포함되어 있고, android{} 블럭에서 안드로이드 라이브러리로서의 설정이 있다.

iOS앱의 경우는 .framework 파일형태로 패키징되어, iosApp 프로젝트 폴더의 iosApp.xcodeproj 파일을 Xcode에서 열어서 iosAPP을 만들수 있다.

이밖에 Javascript, SpringBoot, Desktop(jvm 기반)의 앱 등을 추가할수 있다.

플랫폼 추가

예를 들어 spring-boot 프로그램을 포함하고 싶다면, 루트프로젝터하위에 pring-boot-app(이름은 상관없다)을 만들어 build.gradle.kts를 생성하고
스프링부트 실행에 필요한 의존파일들 과 kotlin 플러그인을 추가하고 src/main/java/com.abcde 폴더에 작업소스를 넣으면된다.
이때 settings.gradle.kts 파일에 include(":spring-boot-app") 이라고 아까 만든 이름을 추가하여 준다.
최종으로 shared/build.gradle.kts 파일에

kotlin {
 ...
    jvm()
 ...
     sourceSets {
        val jvmMain by getting {
            dependencies {
                implementation (kotlin("stdlib"))
                implementation("junit:junit:4.13.1")
            }
        }
     }
}

를 추가하고 shared/jvmMain 폴더도 만들어서 spring-boot 애플리케이션에서 처리할 부분을 추가하자.(밑에는 예이다)

package me.kimtaeho.shared  
  
actual class Platform actual constructor() {  
    actual val platform: String = System.getProperty("os.name")  
}
//주로 맥에서 개발하기 떄문에 Hello, Mac OS X! 가 출력될것이다.

commonMain폴더의 Platform.kt 파일에 정의된 expect Platform 클랙스와 expect platform 변수를 jvmMain 폴더의 Platform.kt파일에서 actual로 구현한것을 볼수 있다.
iosMain 폴더를 보면은 kotlin 에서 iOS프레임워크의 UIKit를 kotlin에서 불러낼수 있도록 지원해주는 것을 알수 있다.

import platform.UIKit.UIDevice  
  
actual class Platform actual constructor() {  
    actual val platform: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion  
}
//Xcode에서 iosApp프로젝트 파일을 열어서 실행해보면 실행된 단말/에뮬레이터의 버젼이 출력된다.

Multiplatform 에 사용할수 있는 라이브러리

KMP, KMM 에서 공통처리를 위한 로직을 만들때 해당기능이 플랫폼에서 사용할수 있는지를 알아야한다.
네트워크같은 경우 네트워크라이브러리를 통해서 멀티플랫폼에 대응하여야는데 Ktor라는 제품을 이용한다.
그밖의 라이브러리 들은
https://github.com/AAkira/Kotlin-Multiplatform-Libraries
여기를 참고하면된다.

Shared 에서 무엇을 공통처리 할것인가

KMP, KMM 라는 솔류션으로 무엇을 어떻게 공통처리 할것인가가 처음에는 크게 와닿지 않을지도 모른다.
공식예제에서는 플랫폼의 OS 이름과 버젼을 출력하는것이 있는데 그런거하려고 멀티플랫폼 이라는 거창한 서비스를 이용하진 않는다.
우리가 앱을 포함하는 하나의 서비스를 만들때, 서버따로, 웹따로 모바일 따로따로 각각의 처리로직을 각각의 팀에서 개발해왔다.
예를 들어, API서버에 요청을 하는 처리가 있다고 할때, Android와 iOS각각의 소스코드에서 각각 필요한 라이브러리를 사용하여 API로부터 데이터를 호출하고 처리한다. 이때 Android, iOS에서는 API에서 가져오는 데이터(JSON ,XML등)을 각각 처리하도록 하는데 이과정에서 개발자들이 서로 다른 처리를 하는경우가 종종생긴다.
KMP 에서는 이러한 공통된 처리를 shared라는 한군데서 처리할수있도록 하여 복잡한 중복로직을 개발하는 공수를 덜게 해준다.
물론 각 플랫폼의 화면처리에 관한것은 플랫폼 고유의 처리에 맡기고 있다.
이렇게 Shared 는 프로그램의 좀더 깊숙한 부분에서 처리되는 비지니스 로직을 담당하도록 하여 개발시간, 프로그램 테스트 시간을 단축시켜준다.

그럼 다시, Shared 에서 무엇을 공통처리 할 것인가 라고 묻는다면
네트워크처리, 도메인로직 처리, 레포지토리 처리, 이벤트로그(분석용) 처리 등을 플랫폼에서 공통으로 사용할 로직들이라고 할수 있다.

0 comments:

댓글 쓰기