Kotlin DSL을 이용하여 동기/비동기 뭐시기 만들어보기

내가 하고 싶은 것은 다음과 같다.

  1. sequential (동기 순차 실행)과 parallel (비동기 병렬 실행)이 존재
  2. sequential과 parallel은 중첩될 수 있음
  3. sequential {…} 안의 sequential과 parallel은 순차실행됨.
  4. parallel {…} 안의 sequential과 parallel은 병렬실행됨.
  5. await 같은 명시적인 join 표시 없이 스코프가 끝나면 알아서 join

대충 클로드에게 물어봤더니 잘 짜줌.

원래는 parallel도 별도의 래퍼 없이 하고 싶었는데 생각해보니 비동기면 그게 불가능하니 어쨋든 래퍼는 필요함

AsyncResult<T>

앞서 말한 parallel에서 사용할 결과 래퍼이다.

data class AsyncResult<T>(internal var raw: Any? = UNSET) {
 internal object UNSET // *1

 @Suppress("UNCHECKED_CAST")
 operator fun <T> AsyncResult<T>.getValue(thisRef: Any?, property: KProperty<*>): T {
     return raw as T
 }

 internal fun set(v: T) {
  raw = v
 }
}

*1: object로 생성한 객체는 클래스 안에 있어도 자동으로 static (companion object)가 된다고 한다.

AsyncScope.kt

저걸 이용해서 Kotlin DSL을 만든다.

@DslMarker
annotation class AsyncDsl


@AsyncDsl
object SequentialScope {
    suspend fun <R> sequential(block: suspend SequentialScope.() -> R): R = SequentialScope.block()

    suspend fun <R> parallel(block: suspend ParallelScope.() -> R): R = coroutineScope {
        ParallelScope(this).block()
    }

    suspend fun <R> task(block: suspend () -> R): R = block()
}

@AsyncDsl
class ParallelScope(private val scope: CoroutineScope) {
    fun <R> sequential(block: suspend SequentialScope.() -> R): AsyncResult<R> {
        val result = AsyncResult<R>()
        scope.launch {
            result.set(SequentialScope.block())
        }
        return result
    }

    fun <R> parallel(block: suspend ParallelScope.() -> R): AsyncResult<R> {
        val result = AsyncResult<R>()
        scope.launch {
            result.set(coroutineScope {
                ParallelScope(this).block()
            })
        }
        return result
    }

    fun <R> task(block: suspend () -> R): AsyncResult<R> {
        val result = AsyncResult<R>()
        scope.launch {
            result.set(block())
        }
        return result
    }
}

suspend fun <R> sequential(block: suspend SequentialScope.() -> R): R = SequentialScope.block()
suspend fun <R> parallel(block: suspend ParallelScope.() -> R): R = coroutineScope {
    ParallelScope(this).block()
}

테스트

class AsyncScopeTests {
    private val logger = Logger.getLogger(AsyncScopeTests::class.qualifiedName)

    data class Config(val userId: UUID, val postId: UUID)
    data class User(val userId: UUID, val name: String)
    data class Post(val postId: UUID, val title: String)

    private suspend fun randomDelay(maxMillis: Long = 1000) {
        val randomDelay = Random.nextLong(maxMillis)
        delay(randomDelay.milliseconds)
    }

    private suspend fun <R> withLogging(title: String, block: suspend () -> R): R {
        logger.info("$title started at ${LocalDateTime.now()}")
        val result = block()
        logger.info("$title finished at ${LocalDateTime.now()}")
        return result
    }

    private suspend fun loadConfig(): Config = withLogging("loadConfig") {
        randomDelay(100)
        Config(userId = UUID.randomUUID(), postId = UUID.randomUUID())
    }

    private suspend fun fetchUser(config: Config): User = withLogging("fetchUser") {
        randomDelay(100)
        User(config.userId, "John Doe")
    }

    private suspend fun fetchPost(config: Config): Post = withLogging("fetchPost") {
        randomDelay(500)
        Post(config.postId, "A Brief History of Time")
    }

    @Test
    fun testAsyncScope(): Unit = runBlocking {
        val (user, post) = sequential {
            val config = task { loadConfig() }

            val (user, post) = parallel {
                val user = task { fetchUser(config) }
                val post = task { fetchPost(config) }
                user to post
            }

            user to post
        }

        println(
            """
            User: $user
            Post: $post
        """.trimIndent()
        )
    }
}

코드상으로는 랜덤이지만 고정으로 딜레이를 줬다 (100,100,500)

loadConfig과 fetchUser,fetchPost는 순차이므로 loadConfig이 끝난 다음에 fetch*가 실행되었고
fetchUser와 fetchPost는 병렬호출이므로 똑같이 시작했다가 fetchUser는 100ms 대기하니 로그에는 100ms정도 흘렀고, fetchPost는 500ms대기하니 0.5초만큼 늘었다.

하지만 sequential은 동기호출이니 fetchPost가 다 끝나고야 최종 정보가 나오고, UNSET으로 나오는 경우는 없어진다.

생각해보니 이것도 좀 문제가 있네;

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다