[Android] Coroutine 이해하기 (3) : 코루틴 suspend

uuranus·2024년 1월 23일
0
post-thumbnail

콜백과 코루틴

  • 비동기 프로그래밍을 할 경우 비동기 작업에 대한 결과처리는 콜백형태로 구현한다.

하지만, 비동기 요청 후 다시 비동기 요청, 다시 비동기 요청을 하게 된다면 콜백의 들여쓰기는 계속 깊어져 가독성이 떨어지게 된다.

예를 들어, 아침 루틴을 프로그래밍한다고 해보자. 아침에 일어나서 씻고 밥 먹고 준비하고 학교를 가야하는데 각각의 과정은 오래걸리기 때문에 비동기로 처리한다고 하자.

wakeup() { 씻은 상태 ->
	eatBreakfast() { 밥 먹은 상태 ->
    	prepare() { 준비한 상태 ->
        	goToSchool()
        }
    }
}

아마 콜백으로 처리한다면 이런 식으로 구현이 될 것이다.

만약 이를 코루틴으로 호출을 한다면?


suspend fun morningRoutine() : Routine {
	val wakeupTime = wakeup()
    val breakfast = eatBreakfast()
    val outfit = prepare()
    val transportation = goToSchool()
	return transportation
}

이런 식으로 마치 동기 프로그래밍하듯이 작성을 할 수 있게된다.
어떻게 이렇게 작성을 할 수 있는가?
suspend 키워드를 붙였기 때문이다.

suspend

suspend 키워드가 뭐길래 콜백을 없애주는 것일까?

  • CPS 변환과 상태 기계를 활용해서 코루틴을 구현하기 때문이다.

CPS 변환

  • CPS 변환은 Continuation Passing Style의 줄임말이다.
  • Continuation을 전달하여 현재까지 실행된 데이터값들과 다음에 실행되어야 할 일이 무엇인지를 알 수 있다.
  • Continuation을 일종의 콜백으로 생각할 수 있다.
  • suspend 키워드를 붙이면 코틀린 컴파일러가 내부적으로 Continuation을 만들어서 매개변수에 추가한다.
  • 그리고 Continuation은 현재 suspend 함수가 종료된 후 반환하는 값을 받는다.

위 코루틴 함수를 CPS로 변환한다면

fun morningRoutine(completion: Continuation<Any?>) {
	val wakeupTime = wakeup()
    val breakfast = eatBreakfast()
    val outfit = prepare()
    val transportation = goToSchool()
	return completion.resumeWith(transportation)
}

다음과 같이 suspend 키워드가 없어지고 Continuation이 추가가 되었다.
그리고 반환값이 없어지고 completion이 resumeWith를 통해서 리턴값을 받게 되었다.

Continuation의 타입이 Any?인 이유는 여러 suspend 함수에 대한 리턴값들을 다 받아야 하기 때문에 최상위 타입으로 명시하는 것 같다.

근데 suspend되는 지점이 result값으로는 구분하기 힘들텐데 여러군데일 경우 현재 어디까지 실행되었는지는 어떻게 아는 것일까?

StateMachine

  • StateMachine으로 해결할 수 있다.
  • 말 그대로 상태를 통해서 현재 상황을 파악할 수 있는 것이다.
fun morningRoutine(completion: Continuation<Any?>) {
	when(label) {
  		0 -> {
  		  	val wakeupTime = wakeup()
  		}
  		1 -> {
  		 	val breakfast = eatBreakfast()
  		}
  		2 -> {
  			val outfit = prepare()
  		}
  		3 -> {
  			val transportation = goToSchool()
  		}
  		4 -> {
  			completion.resumeWith(transportation)
  		}    
}

이렇게 label이라는 값을 통해서 현재 어느 상태까지 왔는지를 파악한다.
suspend 될 수 있는 지점를 나눠서 코루틴 컴파일러가 이렇게 when으로 나눠준다.

label은 어디서 나온 걸까?
Continuation이 가지고 있다.

val continuation = ContinuationImpl(var0) {
    		val result: Any? = null
            val label = 0
  			//suspend 시 필요한 변수들 값
  			val wakeupTime = 0
  			val breakfast = 0
  			val outfit: Any? = null

            override fun invokeSuspend(result: Any?) {
               this.result = result
               return morningRoutine(this)
            }
         }

다음과 같이 Continuation을 구현한 ContinuationImpl이 label과 suspend 함수가 종료되고 반환될 때 저장할 값 result를 가지고 있다.
그리고 suspend 할 때 가지고 있어야 하는 변수들, 예를 들어 prepare를 호출할 때 wakeupTime과 breakfast값이 있어야 하므로 이 값들도 Continuation이 가지고 있는다.
디컴파일하면 L$0, I$0 이런 값으로 설정이 되는데 이해하기 쉽게 변경했다.

invokeSuspend가 resumeWith의 역할을 하는 것 같다.

이를 통해 다시 when 문을 구성해보면

fun morningRoutine(completion: Continuation<Any?>) {
	val continuation = ContinuationImpl(completion)
  	when(continuation.label) {
  		0 -> {
  			continuation.label = 1
  		  	val wakeupTime = wakeup(continuation)
  		}
  		1 -> {
  			continuation.wakeupTime = result.toInt()
  			continuation.label = 2
  		 	val breakfast = eatBreakfast(continuation)
  		}
  		2 -> {
  			continuation.breakfast = result.toInt()
  			continuation.label = 3
  			val outfit = prepare(continuation)
  		}
  		3 -> {
  			continuation.outfit = result.toString()
  			continuation.label = 4
  			val transportation = goToSchool(continuation)
  		}
  		4 -> {
  			val transportation = Transportation(continuation.wakeupTime, ... result.toString())
  			return transportation
  		} 
  		else ->  throw IllegalStateException()
}

동작 순서

  1. morningRoutine 실행
  2. label이 0이므로 label을 1로 바꾸고 wakup 호출
  3. wakeup에서 종료되고 continuation.resumeWith() (invokeSuspend)를 호출
  4. invokeSuspend가 호출되면서 다시 morningRoutine을 실행
  5. 이번에는 label이 1이 되었기 때문에 label을 2로 바꾸고 eatBreakfast 호출
  6. 3,4와 마찬가지로 실행
  7. label이 2가 되었기 때문에 label을 3으로 바꾸고 prepare() 실행
  8. 6,7 반복
  9. label이 4이므로 transportation 클래스 만들고 리턴

세줄 요약

  1. 코루틴은 suspend 키워드를 통해 콜백이 아닌 일반 동기 프로그래밍처럼 코딩을 할 수 있다.
  2. suspend 키워드는 CSP를 통해 내부적으로 Continuation을 만들고 이는 콜백처럼 작동한다.
  3. Continuation은 내부적으로 suspend 함수가 종료되었을 때 결과값을 받을 result, 다음 실행순서를 확인할 label, 필요한 중간값 변수들을 가고 있는 State Machine이다. 함수가 종료될 때 Continuation.resumeWith를 호출하여 결과값과 함께 다시 함수를 호출한 코루틴으로 돌아간다.
profile
Frontend Developer

0개의 댓글