본문 바로가기

안드로이드/Kotlin

[Kotlin] 코틀린 공식 문서 - Coroutines Guide, Asynchronous programming Techniques

개요

해당 포스트는 Coroutines guide | Kotlin (kotlinlang.org)를 번역한 게시글 입니다.

 

Coroutines Guide

언어로서 코틀린은 표준 라이브러리에서 최소한의 저수준 API를 제공하여 여러 다른 라이브러리가 코루틴을 활용할 수 있도록 한다. 유사한 기능을 가진 다른 언어들과 달리 async/await은 Kotlin 만을 위한 키워드가 아니며 표준 라이브러리의 일부도 아니다. 또한, 코틀린의 suspending function이라는 개념은 futures/promises보다 비동기 작업에 대해 더 안전하고 오류 가능성이 적은 쉬운 추상화를 제공한다.

 

 

Asynchronous programming Techniques 

여러 해 동안 개발자들은 blocking으로 부터 앱을 어떻게 적절히 실행해야 좋을 지는 늘 문제였다. 앱, 웹, 백엔드 어느 개발이던 사용자가 기다리거나 보틀넥이 발생하는 것을 피하는 것을 원한다.

 

이러한 문제의 해결책은 크게 다음과 같이 나뉜다.

  • Threading
  • Callbacks
  • Futures, Promises, and others
  • Reactive Extensions
  • Coroutines

코루틴이 무엇인지 설명하기 전에 다른 해결책을 소개한다.

 

Threading

쓰레드는 앱을 blocking으로 부터 방지하는 잘 알려진 접근법이다.

fun postItem(item: Item) {
    val token = preparePost()
    val post = submitPost(token, item)
    processPost(post)
}

fun preparePost(): Token {
    // makes a request and consequently blocks the main thread
    return token
}

preparePost는 오랜 시간이 걸리는 프로세스이고 UI를 차단할 수도 있는 코드라고 가정하자. 가능한 방법 중 하나는 스레드를 분리하여 실행시키는 것이다. 이를 통해 UI를 blocking으로 부터 피할 수 있다. 이는 매우 흔한 테크닉이지만 여러 문제점이 있다.

  • 스레드는 결코 비용이 저렴하지 않다. 문맥 교환이 필요함.
  • 스레드는 유한하다. 스레드의 수는 OS에 의해 제한된다.
  • 스레드가 항상 사용가능한 것은 아니다. JS 같은 언어는 스레드를 지원하지 않는다.
  • 스레드를 활용하는 것은 쉽지 않다. 디버깅도 어렵고, 멀티스레딩 상황에서 경쟁 상태를 제어하는 것도 쉽지 않다.

 

Calbacks

콜백을 사용하면 한 함수를 다른 함수의 파라미터로 전달하고 프로세스가 완료되면 이 함수를 호출하는 것이 아이디어이다.

fun postItem(item: Item) {
    preparePostAsync { token ->
        submitPostAsync(token, item) { post ->
            processPost(post)
        }
    }
}

fun preparePostAsync(callback: (Token) -> Unit) {
    // make request and return immediately
    // arrange callback to be invoked later
}

꽤 고급스러운 해결책이지만, 여러 이슈가 존재한다:

  • 콜백 지옥의 문제. 콜백에 대한 콜백이 겹쳐 이해하기 어려운 코드가 생성된다.
  • 에러 핸들링이 복잡하다. 여러 콜백이 중첩된 경우 이러한 오류 처리를 복잡하게 만든다.

 

Futures, Promises, and others

해당 아이디어는 우리가 어떤 것을 호출했을 때 언젠가 Promise라는 객체와 함께 반환 될 것며 그 다음에 작동 될 수 있다는 개념이다.

fun postItem(item: Item) {
    preparePostAsync()
        .thenCompose { token ->
            submitPostAsync(token, item)
        }
        .thenAccept { post ->
            processPost(post)
        }

}

fun preparePostAsync(): Promise<Token> {
    // makes request and returns a promise that is completed later
    return promise
}

이 경우에는 다음과 같은 프로그래밍 방식에 일련의 변경이 필요하다.

  • 서로 다른 프로그래밍 모델. 콜백과 유사하게 프로그래밍 모델은 top-down 명령 방식에서 체인 콜을 활용하는 모델로 이동한다. Loop, exception과 같은 전통적인 프로그래밍 구조는 이 모델에서 유효하지 않다.
  • 서로 다른 APIs. 일반적으로 새로운 API를 완전히 학습해야된다. 위를 예로 들면 thenCompose, thenAccept.
  • 특정한 리턴 타입. 리턴 타입이 실ㅈ제 데이터에서 Promise로 변경된다.
  • 에러 핸들링이 쉽지 않다.

Reactive Extensions

Reactive Extensions(Rx)는 Erik Meijer에 의해 소개되었다. .NET에서 사용되었지만 Netflix가 Java로 포팅하여 이름을 RxJava로 변경하지 전까지는 주로 선택되는 해결책이 아니었다. 그 이후로 RxJS 등과 같은 수 많은 포트가 제공됐다.

 

Rx의 이면에 존재하는 아이디어는 Observable Stream으로 이동하여 데이터를 스트림(무한한 양의 데이터)로 생각하고 이런 스트림을 관찰하는 방식이다. 사실상 Rx는 데이터에 대해 작업할 수 있는 Observer 패턴이다.

 

접근 방식의 측면에서는 Future와 유사하지만 Future는 유한한 이산 요소를 반환하는 것으로 생각할 수 있지만, Rx는 스트리믈 반환한다. 그러나 이전과 유사하게 프로그래밍 모델에 대한 완전히 새로운 사고 방식이 필요하다. "모든 것이 스트림이며 관찰 가능하다."

 

이는 동키 코드를 작성할 때 많은 변화를 의미한다. Futures와 반대되는 한 가지 이점은 많은 플랫폼으로 포팅되었기 때문에 여러 C#, Java, JS 등과 같이 언어에 상관없이 일관된 API 활용할 수 있다.

 

게다가 Rx는 에러 핸들링에 대한 꽤 좋은 접근 방식이다.

 

Coroutines

비동기 코드에 대한 코틀린의 접근법은 일시 중단 가능한 계산의 개념민 코루틴을 활용하는 것이다. 즉, 함수가 특정 시점에서 실행을 일시 중단하고 나중에 다시 시작할 수 있다는 개념이다.

 

코루틴의 이점 중 하나는 개발자에게 blocking 코드와 non-blocking 코드를 작성하는 것이 동일하여 프로그래밍 모델이 변경되지 않는 다는 점이다.

fun postItem(item: Item) {
    launch {
        val token = preparePost()
        val post = submitPost(token, item)
        processPost(post)
    }
}

suspend fun preparePost(): Token {
    // makes a request and suspends the coroutine
    return suspendCoroutine { /* ... */ }
}

이 코드는 오래 걸리는 작업을 메인 스레드의 blocking 없이 실행한다. preparePost는 suspendable function이라 부르며, 그런 의미로 suspend라는 키워드가 앞에 붙었다. 이는 위에서 언급한 듯 함수가 특정 시점에 실행되고 일시 중지되고 다시 시작되는 것을 의미한다.

  • 함수는 동일하게 유지되며 유일한 차이점은 suspend 키워드이고 리턴 타입 또한 변경되지 않는다.
  • 코드는 코루틴을 시작하는 launch 함수를 사용하는 것 외의 특별한 구문 없이 하향식으로 동기식 코드를 작성하는 것과 같이 가능하다.
  • 프로그래밍 모델과 API는 동일하게 유지된다. Loop, Exception 등을 사용할 수 있으며 완전히 새로운 API를 배우기 위해 노력할 필요가 없다.
  • 플랫폼에 종속적이지 않아 JVM, JS 또는 기타 플랫폼을 대상으로하든 작성하는 코드는 동일하며, 내부적으로 컴파일러는 각 플랫폼에 맞게 조정한다.

코루틴은 Kotlin에서 발명한 것이 아니며 새로운 개념이 아닌 수십 년 동안 사용되어왔다. Go와 같은 언어에서 인기가 있다. 하지만 중요한 점은 Kotlin에서 구현되는 방식에 따라 대부분의 기능이 라이브러리에 위임된다는 것이다. 실제로 suspend 키워드 외에 다른 키워드는 언어에 추가되지 않는다. 이는 문법의 일부로 async/await이 존재하는 C#과 같은 언어와는 다소 다르다. Kotlin을 사용하면 이는 그냥 라이브러리 함수일 뿐이다.