프로그래밍/Kotlin

의존성을 가지는 Initializer 만들기

Lou Park 2024. 6. 1. 15:07

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())
}
}
view raw main.kt hosted with ❤ by GitHub

 

먼저 Initializer 인터페이스다.

특별한건 없고, suspend 함수인 create가 있으며 공통적으로 알아야하는 Context를 받아서 Result를 반환한다.

 

class SomeContext
interface Initializer<T> {
suspend fun create(context: SomeContext): Result<T>
}
view raw Initializer.kt hosted with ❤ by GitHub

 

다음은 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)
}
}
}
}
}
}
view raw Initializer.kt hosted with ❤ by GitHub

 

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())
}
}

 

예측했던 대로 잘 수행된다.