Book Study/[Kotlin] 코틀린 코루틴

[Coroutine] 코루틴 빌더(2)

Jwhy 2024. 8. 22. 22:08
코틀린 코루틴을 공부하며 정리한 글입니다.
혼자 공부하고 정리한 내용이며, 틀린 부분은 지적해주시면 감사드리겠습니다 😀

코루틴 빌더

코루틴 빌더에 대한 자세한 내용은 이전 포스팅을 참고해주세요!

동기 / 비동기

우선, 비동기에 대한 정의를 먼저 확인해보자.

컴퓨터 프로그래밍에서 async/await 패턴은 비동기, 비차단 기능이 일반 동기 기능과 유사한 방식으로 구조화되도록 하는 많은 프로그래밍 언어의 구문 기능 구현이다. - (위키백과)

동기와 비동기의 차이가 무엇인지 코드로 간단하게 한 번 살펴보자.

suspend fun main() {  
    val result = measureTimeMillis {  
        doSomething1()  
        doSomething2()  
    }  
    println("total = ${result.toDuration(DurationUnit.MILLISECONDS)}")  
}  

suspend fun doSomething1() {  
    println("Do something1")  
    delay(1000)  
}  

suspend fun doSomething2() {  
    println("Do something2")  
    delay(1000)  
}
Do something1
Do something2
total = 2.02s

위 함수는 일반적인 함수와 같이 동기적으로 실행된다. 즉, doSomething1을 호출하고, 해당 함수를 끝마친 뒤에 doSomething2가 실행되어, 2초에 걸쳐서 프로그램이 종료된다. 그렇다면 위 코드를 비동기로 바꾸면 어떤 결과가 나오게 될까?

suspend fun main() = runBlocking {  
    val result = measureTimeMillis {  
        val r1 = async { doSomething1() }  
        val r2 = async { doSomething2() }  
        println("result = ${r1.await()} ${r2.await()}")  
    }  
    println("total = ${result.toDuration(DurationUnit.MILLISECONDS)}")  
}  

suspend fun doSomething1(): String {  
    delay(1000)  
    return "hello"  
}  

suspend fun doSomething2(): String {  
    delay(1000)  
    return "world"  
}
result = hello world
total = 1.025s

시간을 보면 알 수 있듯, 2초가 걸리던 main 함수가 1초로 단축되었다. 이렇듯, 비동기는 작업을 다른 스레드에게 맡기고, 본 스레드의 업무를 계속해서 진행하는 것이다. 즉, 이전 포스팅에 나왔던, launch와 비슷한 느낌인 것이다.

Async

앞서 작성한 코드를 다시 가져와서 살펴보자.

suspend fun main() = runBlocking {  
    val result = measureTimeMillis {  
        val r1 = async { doSomething1() }  
        val r2 = async { doSomething2() }  
        println("result = ${r1.await()} ${r2.await()}")  
    }  
    println("total = ${result.toDuration(DurationUnit.MILLISECONDS)}")  
}  

suspend fun doSomething1(): String {  
    delay(1000)  
    return "hello"  
}  

suspend fun doSomething2(): String {  
    delay(1000)  
    return "world"  
}

우선, 코드만 보고 알 수 있는 사실을 하나씩 확인해보자.

 

앞서 async 블록은 launch와 비슷하다고 했는데, launch와 제각각으로 실행된다는 것은 비슷하지만, launch 블록은 값을 반환하지 않는다. 반면에 async는 코드에서 볼 수 있는 것과 같이 블록 내부에 작성한 값을 반환하게 된다. 코틀린의 let 스코프와 비슷하게 블록의 마지막 줄을 반환한다.

 

약간의 차이가 있다면, async는 값을 반환할 때, Deferred<T>라는 타입으로 감싼 뒤에 반환하게 된다. 즉, r1을 그대로 쓸 경우 다음과 같이 엉뚱한 값을 반환받게 되는 것이다.

DeferredCoroutine{Active}@6fadae5d

그냥 비동기로 실행하기만 할거면 launch를 사용하겠지만, async의 목적은 비동기로 실행하되, 값을 반환받기 위한 것이기 때문에, Deferred로 감싼 뒤에, 비동기로 실행한 해당 함수가 완료되었는지, 확인하고 값을 꺼내오는 것이 바로 r1.await()이다.

때문에 async 빌더는 시간이 오래 걸리지만, 값을 꼭 받아와야지 다른 함수를 사용할 수 있는 경우에 많이 사용된다.

정리

회사 사내 서비스로 배포 관련 시스템을 개발할 때, async를 사용해, 클라이언트가 응답을 기다리는 시간을 절반 이상으로 줄인 경험이 있다. 당연히 다음과 같은 형태로 사용한다면, async를 사용하는 의미가 없다.

suspend fun deploy() {
    val p1Result = async { part1() }
    p1Result.await()
}

suspend fun part1(){
    delay(3000)
}

이렇게 async를 호출하자마자 바로 await()을 사용하면 해당 함수를 비동기로 실행시킨 의미가 없으니, 구조를 잘 파악해서 사용하는 것이 좋다.