필자는 서버나, 외부 DB가 필요한 Android 프로젝트에 Firebase를 사용하고 있다. Firebase sdk의 경우 Java 코드로 구현되어 있으며, callback을 통해 비동기를 지원하는 것이 특징이다.
필자는 이러한 callback을 SuspendCoroutines를 통해 처리하고 있으며, 해당 SuspendCoroutines에 대해 공부한 내용을 포스팅하려고 한다.
Callback이란 무엇일까?
callback이란 비동기를 구현하기 위한 하나의 방법으로, 어떤 시점에 도달하였거나, 이벤트가 발생하였을 때 실행되는 코드를 의미한다. 가장 쉽게 Android에서 onCreate(), onResume()의 경우 모두 callback 함수라고 볼 수 있다. 이러한 callback을 통해, 특정 지점에 도달하거나 특정 이벤트에 따라 처리를 할 수 있다.
그럼 왜 바꾸는 것이 좋을까 ?
사람들이 callback 패턴을 지양하는 가장 큰 이유는 callback 지옥이라고 불리는 무수한 인덴트이다.
Callback 지옥은 코드의 제어 흐름을 이해하기 어렵게 만들고, 향후 코드를 유지보수하기 어렵게 만든다. 또한 코드가 사용 중인 특정 callback 기반 API와 긴밀하게 결합되어 있으면, 재사용하기 어렵다.
private fun startRegister(){
binding.registerButton.setOnClickListener {
if (passwordVerify == password) {
val request = RegisterRequest(username, password)
RetrofitBuilder.userService.register(request)
.enqueue(object : Callback<RegisterResponse> {
override fun onResponse(
call: Call<RegisterResponse>,
response: Response<RegisterResponse>
) {
val responseBody = response.body()
val responseCode = response.code()
if (responseCode == 201) {
Toast.makeText(loginActivity, "회원가입 되었습니다!", Toast.LENGTH_LONG).show()
loginActivity.goLogin()
} else {
Log.d("Tag", responseCode.toString())
Toast.makeText(loginActivity, "이미 중복된 아이디가 있습니다.", Toast.LENGTH_LONG).show()
}
}
override fun onFailure(call: Call<RegisterResponse>, t: Throwable) {
Toast.makeText(loginActivity, "인터넷 연결이 불안정합니다.", Toast.LENGTH_LONG).show()
}
})
}
else{
Toast.makeText(loginActivity, "비밀번호가 서로 다릅니다.", Toast.LENGTH_LONG).show()
}
}
}
해당 코드는 필자가 안드로이드 개발을 처음 접할 당시, 서버와의 연결 로직을 구성한 코드이다. 해당 코드를 보면, 요청 결과를 받기 위해 콜백을 interface로 제공하고 개발자가 구현체를 만들어 구현하고 있다.
이러한 코드는 성공(회원가입된 상태)까지 도달하기 위해 많은 단계를 명시해야 하며, 또한 각각의 실패를 처리해야 한다.
또한, 만약 해당 함수가 끝난 후 다른 함수를 순차적으로 실행시키기 위해서는 해당 callback에 await() 함수를 통해, callback의 종료 시점을 기다려야 한다.
Kotlin에서는 이러한 상황을 용이하게 대처할 수 있는 SuspendCoroutine과 SuspendCancellableCoroutine api를 제공한다.
SuspendCoroutine
SuspendCoroutne과 SuspendCancellableCoroutine 둘 모두 callback을 coroutines으로 변환하는 기능을 가지고 있다.
suspend inline fun <T> suspendCoroutine(
crossinline block: (Continuation<T>) -> Unit
): T
SuspendCoroutine은 코드에서 확인할 수 있듯, Continuation 객체를 가지는 callback 함수를 매개변수로 가지고 있다. SuspendCoroutine은 Continuation 객체를 사용하여 callback이 호출될 때까지 현재 코루틴을 일시 중단한다. 그리고 callback이 호출되면, Continuation 객체는 코루틴을 재개하고 콜백 결과를 호출 코드로 다시 전달한다.
그럼 Continuation은 뭐에요 ?
Continuation은 resume되었을 때의 동작 관리를 위한 객체로, 연속적인 상태간의 communicator이다. 쉽게 이야기하면 Coroutine 내부에서 Suspend Call이 발생하면 중지되고, Call이 끝난 다음 다시 재개되는데, 중지되었을 때 상태를 가지고 있는 객체가 Continaution이다.
suspend fun fetchData(): String {
return suspendCoroutine { continuation: Continuation ->
val result = "data" // 비동기 작업 처리
if(result == null) {
continuation.resumeWithException( ... )
}
else {
continuation.resum(...)
}
}
}
이런 Contination을 활용하여, resume() 과 resumeWithException()을 통해서 값을 반환해주거나 throw exception을 할 수 있다.
SuspendCancellableCoroutine
그럼 SuspendCancellableCoroutine은 무엇이고, 위의 SuspendCoroutine과는 어떤게 다를까?
inline suspend fun <T> suspendCancellableCoroutine(
crossinline block: (CancellableContinuation<T>) -> Unit
): T
코드로 확인하면, callback 람다의 인자가 Continuation에서 CancellableContinuation으로 변경되었다는 점이다. 이는 위의 Continuation과 무엇이 다를까?
차이점은 Continuation 설명에서, Contination은 Coroutines 내부에서 Suspend Call이 발생하면 Coroutine이 중지되고, 상태를 가지고 있는 객체라고 설명하였다. 그런데, 만약 정지된 순간에서 Coroutines이 취소가 되거나 완료가 되면 어떻게 될까 ?
SuspendCoroutine의 Continuation은 Coroutines에 상황에 관계없이 무조건 작업을 완료하고, resume() 혹은 resumeException을 반환한다.
그러나 SuspendCancellableCoroutine의 CancellableContinuation은 Coroutines이 취소가 되거나 완료가 되면, CancellationException을 발생시킨다. 따라서 더 이상 재개되지 않고 예외를 반환하고 종료가 된다.
그럼 무엇을 써야할까?
대부분 Coroutine에 따라 작업을 취소할 수 있는 SuspendCancellableCoroutine을 사용하는 것을 추천한다. Coroutines에 따라 수행하기 때문에 안정성이 높기 때문이다.
이 외에도 SuspendCancellableCoroutine 문서에 나와 있는 SuspendCancellableCoroutine의 특징을 소개한다.
즉시 취소를 보장한다.
SuspendCancellableCoroutine에서 resume을 수행하면, CoroutineDispatcher에 요청하고, Dispatcher의 스케줄에 따라 실행된다.
만약 CancellableCoroutine의 resume을 실행하여 Dispatcher에 요청을 했는데, 만약 상위 Coroutines이 취소가 되어도 즉각적인 취소를 수행하고, resume은 수행되지 못한 것으로 처리가 된다 .
중지된 코루틴으로부터 자원을 해제할 수 있다.
파일을 열 때 혹은 다른 네이티브 자원을 사용하고 해제를 필요로 하는 경우가 있다. 이 경우 성공과 실패에 모두 자원 해제의 코드를 넣을 수 있지만, 위의 즉시 취소가 수행될 때도 자원이 해제되어야 한다.
이러한 경우 CancellableCoroutine은 두가지 방법을 통해 자원을 해제할 수 있다.
suspendCancellableCoroutine { continuation ->
val resource = openResource() // Opens some resource
continuation.invokeOnCancellation {
resource.close() // Ensures the resource is closed on cancellation
}
// ...
}
다음과 같이 invokeOnCancellation를 명시해, 즉시 취소가 될 때 수행되어야 하는 코드를 작성할 수 있다.
suspendCancellableCoroutine { continuation ->
val callback = object : Callback { // Implementation of some callback interface
override fun onCompleted(resource: T) {
continuation.resume(resource) {
resource.close() // Close the resource on cancellation
}
}
// ...
}
또는 다음과 같이 resume 시 cancel의 경우를 명시하여 취소가 될때 수행되어야 하는 코드를 작성할 수 있다.
참고 문헌
https://www.baeldung.com/kotlin/suspendcoroutine
https://tourspace.tistory.com/442