Android Jetpack 라이브러리 중 하나인 App Startup은 안드로이드 앱 구동에 필요한 초기 설정들을 체계적으로 초기화하는데 유용한 라이브러리다. 아래는 App Startup의 사용 예시 코드인데, dependencies 함수를 보면 예시 코드의 로거 Initializer는 WorkManagerInitializer에 의존하고 있음을 알 수 있다.
// Initializes ExampleLogger. class ExampleLoggerInitializer : Initializer<ExampleLogger> { override fun create(context: Context): ExampleLogger { // WorkManager.getInstance() is non-null only after // WorkManager is initialized. return ExampleLogger(WorkManager.getInstance(context)) } override fun dependencies(): List<Class<out Initializer<*>>> { // Defines a dependency on WorkManagerInitializer so it can be // initialized after WorkManager is initialized. return listOf(WorkManagerInitializer::class.java) } }
꽤나 유용하지만 App Startup 라이브러리의 Initializer는 안드로이드 플랫폼 종속적이며, 비동기처리도 할 수 없다.
안드로이드가 아니더라도 비슷한 종류의 작업을 처리하는 툴이 또 있다.
바로 Gradle! 아래 코드에는 taskX가 이루어지기 위해서는 taskY가 실행되어야함이라는 정보가 있다.
tasks.register("taskX") { dependsOn("taskY") doLast { println("taskX") } } tasks.register("taskY") { doLast { println("taskY") } }
이런 것들에 힌트를 얻어서, 아래 코드 스니펫 처럼 적으면 알아서 의존성에따라 초기화를 시켜주는 Initializer를 만들고 싶었다.
fun main(args: Array<String>) { | |
runBlocking { | |
val initializer = InitializerBuilder { | |
register(NodeA(), "A") | |
register(NodeC(), "C") | |
register(NodeB(), "B") | |
.dependsOn("A") | |
.dependsOn("C") | |
}.build() | |
initializer.initialize(SomeContext()) | |
} | |
} |
먼저 Initializer 인터페이스다.
특별한건 없고, suspend 함수인 create가 있으며 공통적으로 알아야하는 Context를 받아서 Result를 반환한다.
class SomeContext | |
interface Initializer<T> { | |
suspend fun create(context: SomeContext): Result<T> | |
} |
다음은 InitializerBuilder의 구현부다.
ktor를 배껴쓸때 영감을 받아...직접 InitializerBuilder를 생성해서 쓰게 하진않고, Kotlin DSL을 활용하도록 구성했다.
build를 하면 Initializer<Unit>이 만들어지고 등록된 노드들을 순회하며 초기화 작업을 수행하게 된다. 그래프에 있는 노드의 초기화 작업이 실패할경우, 초기화도 실패하게 된다.
class InitializerBuilder internal constructor( | |
private val block: Builder.() -> Unit | |
) { | |
data class Node( | |
val initializer: Initializer<*>, | |
val name: String, | |
val dependencies: MutableList<String> | |
) { | |
fun dependsOn(name: String): Node { | |
this.dependencies.add(name) | |
return this | |
} | |
} | |
private val nodes = mutableListOf<Node>() | |
inner class Builder() { | |
fun register(initializer: Initializer<*>, name: String): Node { | |
val node = Node( | |
initializer = initializer, | |
name = name, | |
dependencies = mutableListOf() | |
) | |
nodes.add(node) | |
return node | |
} | |
} | |
fun build(): Initializer<Unit> { | |
block.invoke(Builder()) | |
return object : Initializer<Unit> { | |
private suspend fun traverse( | |
context: SomeContext, | |
node: Node, | |
initialized: MutableSet<String>, | |
nodesMap: Map<String, Node> | |
) { | |
val dependencies = node.dependencies | |
dependencies.forEach { dependency -> | |
val dependencyNode = nodesMap[dependency] ?: throw IllegalStateException("Dependency [${dependency}] not found") | |
if (dependency !in initialized) { | |
traverse(context, dependencyNode, initialized, nodesMap) | |
} | |
} | |
if (node.name !in initialized) { | |
node.initializer.create(context).map { | |
initialized.add(node.name) | |
}.getOrThrow() | |
} | |
} | |
override suspend fun create(context: SomeContext): Result<Unit> { | |
return kotlin.runCatching { | |
val nodesMap = nodes.associateBy { it.name } | |
val initialized = mutableSetOf<String>() | |
nodes.forEach { node -> | |
traverse(context, node, initialized, nodesMap) | |
} | |
} | |
} | |
} | |
} | |
} |

Gradle 공식 문서를 보다보면 이러한 Task Graph가 있는데, 방금 만든 Initializer를 이용해서 제대로 동작하는지 검증해보자! 왼쪽 그래프대로 구성해보았다.
fun main(args: Array<String>) { runBlocking { val initializer = buildInitializer { register(Node("A"), "A") .dependsOn("B") .dependsOn("C") register(Node("C"), "C") .dependsOn("D") .dependsOn("E") register(Node("B"), "B") register(Node("D"), "D") .dependsOn("Z") register(Node("E"), "E") .dependsOn("Z") register(Node("Z"), "Z") } initializer.create(SomeContext()) } }

예측했던 대로 잘 수행된다.
'프로그래밍 > Kotlin' 카테고리의 다른 글
주기적으로 작업을 모아 처리하는 BatchLoader 구현하기 (1) | 2024.10.29 |
---|---|
[KotlinConf2024] Kotlin 2.0의 새로운 피쳐들 (1) | 2024.09.18 |
Flow.map() vs Flow.transform() (0) | 2023.04.02 |
[Kotlin] 클로저(Closure)에 대해 알아보자 (0) | 2023.03.20 |
Kotlin은 왜 나왔고, 왜 Android 공식언어로 채택되었을까? (0) | 2023.03.19 |