728x90
메인 스레드(Main Thread, UI Thread)란?
- 앱이 실행됐을 때 시스템에서는 메인(Main)이라고 불리는 스레드를 생성한다.
- 메인스레드는 안드로이드 이벤트 생성 및 처리를 담당할 뿐 아니라 안드로이드에서 발생되는 여러 이벤트를 그와 관련된 위젯으로 연동시키는 중요한 역할을 수행한다.
- 또한 안드로이드에서 제공하는 다양한 뷰와 위젯을 표현하는 역할과 사용자로 하여금 그것을 사용할 수 있게 해 주어 UI 스레드라고도 불린다.
앱에 메인 스레드만 존재한다면?
- 앱이 단일 스레드 즉 메인 스레드만 존재하는 모델에서는 형편없는 성능을 낳게 된다.
- 메인 스레드에서 모든 작업을 처리한다면 네트워크 처리 및 데이터베이스 쿼리와 같이 오래 걸리는 작업을 하는 동안 UI와 관련된 작업을 처리하지 못하게 된다.
- 이것이 문제가 되는 이유는 오랜 시간 UI 관련된 작업을 처리하지 못하면 ANR(Application Not Responding)에러가 발생하고 그 즉시 앱이 정지되기 때문이다.
동시성(Concurrency)
- Main-Safe하게 앱의 여러 작업을 수행하는 방법의 하나로, 동시성을 사용하는 방법이 있다.
- 하지만 동시성 코드를 제대로 작성하는 것은 좀처럼 간단한 일이 아닙니다. 경쟁 상태, 원자성 위반, 교착 상태, 라이브락과 같은 동시성 이슈들이 개발자들을 괴롭히기 때문이다.
우리는 이 중에서 Coroutine, 그리고 그 Coroutine는 내부적으로 어떻게 동시성(Concurrency)을 제공하는지 살펴보자.
코루틴이 JVM에서 동작하는 방법
There is no magic
- 코루틴은 디컴파일되면 일반 코드일 뿐이다.
- Continuation Passing Style(CPS, 연속 전달 방식) 이라는 형태의 코드로 전환한다.
Basic Problem
fun postItem(item: Item) {
val token = requestToken()
val post = createPost(token, item)
processPost(post)
}
- 위 코드는 서버에서 토큰을 가져와서 게시물을 포스트 한 다음, 포스트 완료처리를 하는 세 가지 연산을 하는 코드이다.
- 이것이 JVM에서 Continuation Passing Style(CPS)로 내부적으로 컴파일할 때 아래와 같이 바뀐다.
Continuation Passing Style(CPS)
fun postItem(item: Item) {
requestToken { token ->
val post = createPost(token, item)
processPost(post)
}
}
- Continuation Passing Style은 결과를 호출자에게 직접 반환하는 대신 Callback 방식으로(Continuation)으로 결과를 전달한다.
Suspend 함수
suspend fun createPost(token: Token, item: Item): Post { … }
- createPost(...) suspend 함수를 하나를 만들었을 때, 이 함수는 일시 중단 및 재개될 수 있는 함수가 된다.
- 어떻게 이것이 가능한지 살펴보자.
// suspend fun createPost(token: Token, item: Item): Post { … }
↓
// Java/JVM
Object createPost(Token token, Item item, Continuation<Post> cont) { … }
- 내부적으로는 JVM에 들어갈 때 바이트코드로 컴파일되면서 같은 createPost(…)인데 Continuation이 생성되어 Continuation Passing Style로 변환된다.
- 호출했던 함수의 끝에 매개변수가 하나 추가되서 Continuation이라는 객체를 넘겨주는 것으로 변환되는 것이다.
suspen fun postItem(item: Item) {
// LABEL 0
val token = requestToken()
// LABEL 1
val post = createPost(token, item)
// LABEL 2
processPost(post)
}
- 먼저 Labael이라는 작업을 하게 되는데 코루틴에서 순차적으로 작성했던 코드들이 suspend 함수가 되면 컴파일할 때 Label이 찍히게 된다.
- 이 함수가 재개되어야 하는데, 재개될 때 필요한 Suspension Point(중단 및 재개 지점)가 요구된다.
- 그래서 이 지점들을 Label로 찍어놓는 것이다. 이런 작업을 코틀린 컴파일러가 내부적으로 하게 된다.
suspend fun postItem(item: Item) {
switch (label) {
case 0:
val token = requestToken()
case 1:
val post = createPost(token, item)
case 2:
processPost(post)
}
}
대략적으로 위와 같은 형태가 되는데, 작성했던 함수가 내부적으론 'switch-case'문처럼 바뀌어 case문이 3개가 생성되고 세 번을 실행하는 것을 알 수 있다. 함수를 실행할 때 0번이든, 1번이든, 2번이든 함수를 재개할 수 있는 지점이 생긴 것이다. 그리고 이 함수를 호출한 지점은 중단점이 될 수도 있다.
Label들이 다 완성되고 나면 Continuation Passing Style로 변환을 하게 된다.
fun postItem(item: Item, cont: Continuation) {
val sm = object : CoroutineImpl { … }
switch (sm.label) {
case 0:
val token = requestToken(sm)
case 1:
val post = createPost(token, item, sm)
case 2:
processPost(post)
}
}
- Continuation이라는 객체가 있고, 매 번 함수를 호출할 때마다 continuation을 넘겨준다.
- Continuation은 Callback 인터페이스 같은 것으로, 재개를 해주는 인터페이스를 가진 객체인 것이다.
위의 코드에서 sm이라고 하는 것은 'state machine'을 의미하는데, 각각의 함수가 호출될 때 상태(지금까지 했던 연산의 결과)를 같이 넘겨줘야 한다. 이 'state machine'의 정체는 결국 Continuation이고, Continuation이 어떠한 정보값을 가진 형태로 Passing이 되면서 코루틴이 내부적으로 동작하게 되는 것이다.
정리
- Coroutine은 Continuation을 주고 받는 CPS 패러다임을 사용한다.
- 컴파일러는 suspend fun의 시그니처를 변경하는데, 이때 매개변수에 자동으로 Continuation을 추가한다.
- 컴파일러는 suspend fun의 내부 코드들을 분석하여 중단 가능 지점을 찾아 구분한다.
- 컴파일러는 다음 실행 지점을 나타내는 label과 내부 변수들을 관리하는 상태머신 클래스를 생성한다.
즉 컴파일러가 suspend 키워드를 만나면 CPS 패러다임을 구현하여, Continuation이라는 일종의 콜백을 주고 받도록 코드를 변환해준다는 것이다.
'Android' 카테고리의 다른 글
안드로이드 [Kotlin] - Jetpack Compose 코트랩 실습(1) (0) | 2023.05.07 |
---|---|
안드로이드 [Kotlin] - Jetpack Compose 시작 (명령형 UI 구성 vs 선언형 UI 구성) (0) | 2023.04.09 |
안드로이드 [Kotlin] - UI State 관리 (0) | 2023.01.18 |
안드로이드 [Kotlin] - Bye LiveData, Hi StateFlow! (0) | 2023.01.08 |
안드로이드 [Kotlin] - 프로가드(Proguard) 설정하기 (0) | 2022.12.26 |