해당 글은 사내에서 세미나를 한 것을 정리한 것이다.
핵심목표
- 코루틴의 작동 방식에 대해 간략하게 이해하기
- 기존에 사용하던 코루틴의 적절한 사용 여부 확인하기
- 잘못 사용된 경우, 그 수정 방법에 대해 고민하기
코루틴을 배워야 하는 이유?
예시를 들어보자.
아래 예시는 AP로부터 뉴스를 가지고 와서 정렬한 다음, 스크린에 띄우는 로직을 구현하는 경우다.
fun onCreate() {
val news = getNewsFromApi()
val sortedNews = news
.sortedByDescending{ it.publishedAt }
view.showNews(sortedNews)
}
위의 예제는 안타깝게도 위와 같은 방식으로 개발할 수 없다.
이유는 안드로이드에서는 하나의 앱에서 뷰를 다루는 스레드가 단 하나만 존재하기 때문에 Main쓰레드는 블로킹되서는 안된다.
때문에 getNewsFromApi() 함수가 쓰레드를 블로킹 할 것이고, ANR 이 생길 것 이다.
위 문제를 해결하기 위해 아래와 같이 수정해보자.
아래는 블로킹이 가능한 스레드를 먼저 사용하고, 이후에 메인 스레드로 전환하면 된다.
fun onCreate() {
thread {
val news = getNewsFromApi()
val sortedNews = news
.sortedByDescending{ it.publishedAt }
runOnUiThread {
view.showNews(sortedNews)
}
}
}
그러나 이러한 방식에도 문제가 있다.
예를 들면 뷰를 열었다가 재빨리 닫으면 뷰가 잠깐 열려있는 동안 스레드가 다수 생성되어 생성된 스레드를 제거하지 않으면 불필요한 작업을 게속 수행하다 뷰를 수정하려고 시도할 것이다. 이렇게 되면 예상하기 어려운 결과가 발생할 수 있다.
이러한 문제점은 아래와 같이 정리할 수 있다.
- 스레드가 실행 되었을 때 멈출 수 있는 방법이 없어 메모리 누수가 이어 질 수 있다.
- 스레드를 너무 많이 생성 하면 비용이 많이 든다.
- 스레드를 자주 전환하면 복잡도가 증가하며 관리하기 어렵다.
- 코드가 쓸데없이 길어지고 이해하기 어려워진다.
위의 문제를 해결하기 위해 더 나은 방법을 생각해보자.
다음은 콜백을 사용하여 Non-Blocking을 만들고 함수작업이 끝났을때 호출될 콜백 함수를 넘겨주는 것이다.
fun onCreate() {
getNewsFromApi() {news ->
val sortedNews = news
.sortedByDescending{ it.publishedAt }
view.showNews(sortedNews)
}
}
이러한 방법은 스레드를 사용하지 않지만 중간에 작업을 취소할 수 없는 단점이 있다.
물론 취소할 수 있는 콜백함수를 만들 수 있지만 쉬운 일이 아니다. 콜백 함수 각각에 대해 취소 할 수 있도록 구현해야 하고 취소하기 위해서는 모든 객체를 분리해야 한다.
또한 세군데에서 데이터를 얻어오는 다음과 같은 예제를 생각해보자.
fun showNews() {
getConfigFromApi { config ->
getnewsFromApi(config) { news ->
getUserFromApi { user ->
view.showNews(user, news)
}
}
}
}
위 코드는 다음과 같은 이유때문에 완벽한 해결책이 될 수 없다.
- 뉴스를 얻어오는 작업과 사용자 데이터를 얻어오는 작업은 병렬로 처리할 수 있지만 콜백으로 처리하기 매우 어렵다.
- 취소할 수 있도록 구현 하려면 매우 어렵다.
- 들여 쓰기가 많아져 코드가 읽기 복잡해진다.
- 콜백을 사용하면 작업의 순서를 다루기 어렵다.
이를 해결하기 위해 RxJava 와 리액티브 스트림을 사용해보자.
아래 예시는 RxJava를 사용한 예시이다.
이 예시는 메모리 누수도 없고, 취소도 가능하며, 스레드를 적절하게 사용하고 있다.
fun onCreate(){
disposables += getNewFromApi()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.map {news ->
news.sortedByDescending { it.publishedAt }
}
.subscribe { sortedNews ->
view.showNews(sortedNews)
}
}
그러나 이러한 예시는 복잡하다는 단점이 있다.
RxJava를 사용하기 위해 subscribeOn, observeOn, map, subscribe와 같은 함수들을 배워야 한다.
또한 취소하는 작업을 명시적으로 표시해야 한다.
그렇다면 우리의 코루틴은 어떤가?
코루틴의 핵심 기능은 코루틴을 특정 지점에서 멈추고 이후에 재개 할 수 있다는 것이다.
이러한 핵심을 이용하면 우리가 짠 코드를 메인 스레드에서 실행하고 API 에서 데이터를 불러올때 잠깐 중단 시킬 수 있다.
val scope = CoroutineScope(Dispatcher.Main)
fun onCreate() {
scope.launch { updateNews() }
scope.launch { updateProfile() }
}
suspend fun updateNews() {
showProgressBar()
val news = getNewsFromApi()
val sortedNews = news
.sortedByDescending { it.publishedAt }
view.showNews(sortedNews)
hideProgressBar()
}
suspend fun updateProfile() {
val user = getUserData()
view.showUser(user)
}
위의 코드를 보면 updatedNews와 updateProfile 함수가 메인 스레드 내에서 각각의 코루틴으로 실행되는 걸 보여주고 있다.
두 함수가 한 스레드 내에서 넘나 들며 실행한다.
이유는, 스레드가 블로킹 되는 것이 아닌 코루틴이 중단되기 때문이다.
코루틴을 정의하면, 중단 했다가 다시 실행할 수 있는 컴포넌트(프로그래밍에서 재사용 가능한 각각의 모듈)라고 할 수 있다.
그렇다면 위의 예시에서 세개의 엔드 포인트를 호출해야 하는 문제는 어떻게 해야할까?
fun showNews() {
viewModelScope.launch {
val config = getConfigFromApi()
val news = getNewsFromApi(config)
val user = getUserFromApi()
view.showNews(user, news)
}
}
위의 코드도 좋아 보이지만, 작동하는 방식은 효율적이지 못하다.
호출은 순차적으로 일어나기 때문에 각 호출이 1초씩 걸린다면 전체 함수는 3초가 걸린다.
만약 위와 같이 API가 병렬로 호출했다면 3초대신 2초만에 작업을 끝낼 수 있다.
이럴때 async와 같이 코틀린이 제공하는 라이브러리를 사용한다.
async는 요청을 처리하기 위해 만들어진 코루틴을 즉시 시작하는 함수로, await와 같은 함수를 호출하여 결과를 기다린다.
fun showNews() {
viewModelScope.launch {
val config = async {getConfigFromApi() }
val news = async {getNewsFromApi(config.await()) }
val user = async { getUserFromApi() }
view.showNews(user.await(), news.await())
}
}
또 다른 예시를 보자.
코루틴을 사용하면 for-루프 나 컬렉션을 처리하는 함수를 사용할때 블로킹 없이 구현 가능하다.
아래 코드는 각 페이지를 병렬로 다운로드 하거나 한장씩 보여주는 방식을 구현한 코드다.
//모든 페이지를 동시에 받아온다.
fun showAllNews() {
viewModelScope.launch {
val allNews = (0 until getNumberOfPages())
.map {page -> async {getNewsFromApi(page) } }
.flatMap { it.await() }
view.showAllNews(allNews)
}
}
//페이지별로 순차적으로 받아온다.
fun showPagesFromFirst() {
viewModelScope.launch {
for (page in 0 until getNumberOfPages()) {
val news = getNewsFromApi(page)
view.showNextPage(news)
}
}
}
코루틴의 중단은 어떻게 가능할까?
중단 함수는 코틀린 코루틴의 핵심이다. 비디오 게임을 하다가 멈추는 상황이랑 비슷하다. 체크 포인트에서 게임을 저장하고 종료한 뒤, 사용자와 컴퓨터는 각자의 일을 하다가 다시 게임을 재개하고 저장한 체크포인트에서 시작할 수 있다. 이는 코루틴의 철학과 비슷하다.
코루틴은 중단되었을때 Continuation 객체를 반환한다. 이 객체는 게임을 저장하는것과 같다.
또한 Continuation 객체를 통해 정지된 코루틴을 재개 가능하다.
여기서 스레드와 많이 다른 것을 알 수 있는데, 스레드는 저장이 불가능하고 멈추는 것만 가능하다. 이러한 점에서 코루틴이 훨씬 강력하다. 코루틴이 중단 되었을 때 코루틴은 어떤 자원도 사용하지 않는다.
또 다른 말로는 suspend function은 코루틴이 아니다. 코루틴을 일시 정지 시킬 수 있는 함수일 뿐이다.
Continuation란
위에 나왔던 Continuation에 대해 궁금할 것이다. 아래 Continuation Interface 함수를 보자.
kotlin에서는 Continuation 객체를 통해 다음에 수행할 중단/재개 시점을 Context에 저장하여 관리한다.
즉, Continuation 객체는 Callback Interface를 일반화한 객체라고도 볼 수 있다.
@SinceKotlin("1.3")
public interface Continuation<in T> {
// 이 Continuation 에 해당하는 CoroutineContext
public val context: CoroutineContext
// 마지막 중단점의 반환값으로 성공 또는 실패한 결과를 전달하는 해당 코루틴을 재시작한다.
public fun resumeWith(result: Result<T>)
}
@SinceKotlin("1.3")
@InlineOnly
public inline fun <T> Continuation(
context: CoroutineContext,
crossinline resumeWith: (Result<T>) -> Unit
): Continuation<T> =
object : Continuation<T> {
override val context: CoroutineContext
get() = context
override fun resumeWith(result: Result<T>) =
resumeWith(result)
}
이렇게 Continuation은 프로세스의 실행에서 다음에 수행할 위치를 구조화할 수 있기 때문에, Exception, Generator, Coroutine 등의 프로그래밍 언어에서 제어 메커니즘을 인코딩하는데 효율적이다.
이제 작업이 재개되는 원리를 사용 예제를 통해 보자.
중단 함수는 말 그대로 코루틴을 중단 할 수 있는 함수이다. 이는 중단 함수가 반드시 코루틴에 의해 호출되어야 함을 의미한다.
아래의 간단한 예시가 있다.
suspend fun main() {
println("Before")
println("After")
}
//Before
//After
이 코드에 중간에 중단하면 어떻게 될까?
코틀린 라이브러리에서 제공하는 suspendCoroutine함수를 사용해보자.
suspendCoroutine의 람다 표현식 안에는 컨티뉴에이션의 객체를 인자로 받는다.
suspend fun main() {
println("Before")
suspendCoroutine<Unit> { continuation ->
println("Before too")
}
println("After")
}
//Before
//Before too
suspendCoroutine은 중단하기 전에 컨티뉴에이션의 객체를 사용할 수 있게 한다.
람다 함수는 컨티뉴에이션 객체를 저장한 뒤 코루틴을 다시 실행할 시점을 결정하기 위해 사용된다.
컨티뉴에이션 객체를 이용해 아래와 같이 곧바로 실행할 수 있다.
suspend fun main() {
println("Before")
suspendCoroutine<Unit> { continuation ->
continuation.resume(Unit)
}
println("After")
}
//Before
//After
그렇다면 suspend 와 resume은 꼭 동일한 Thread에서만 이루어져야 할까?
정답은 아니다. 코루틴의 suspend <-> resume은 서로 다른 Thread 에서도 가능하다.
아래 예시를 보면 continueOnAnotherThread에서 다른 쓰레드를 만들고 resume을 했음에도 println("Knights")가 출력된다. 이처럼 suspendCoroutine에서 잠깐 동안 정지(sleep) 된 뒤 재개되는 다른 스레드를 실행시킬 수도 있다.
fun continueOnAnotherThread(continuation: Continuation<Unit>) {
Thread {
Thread.sleep(1000L)
println("(resume on another thread)")
continuation.resume(Unit)
}.start()
}
suspend fun main() {
println("Droid")
suspendCoroutine<Unit> { continuation ->
continueOnAnotherThread(continuation)
}
println("Knights")
}
//////////////////////////////////////////
//Droid
(1초뒤)
//(resume on another thread)
//Knights
종료
Suspend function 중 대표적으로 delay() 함수가 있다.
delay()가 어떻게 구현되어 있는지 확인해보기위해 (물론 실제로 아래 코드보다 복잡하게 되어 있지만) 간략하게 컨셉만 가져와 봤다.
timeMillis뒤에 스케쥴에 의해서 새로운 쓰레드에서 continuation에 resume을 달아줬다.
이렇게 했을때 기존에 알고 있는 Thread.sleap() 함수와 차이가 있다.
앞서 말했듯 Thread.sleap()는 해당 쓰레드를 모두 점유를 해서 다른 자원이 쓰레드에서 동작할 수 없는데
Suspend function의 delay()는 suspend한 쓰레드에서 마저 다른 코루틴을 동작하고, timeMillis뒤에 스캐쥴에 의해서 새로 생성된 쓰레드에 쓰레드 내부적으로 resume을 해주기 때문에 Thread.sleap() 함수보다 훨씬 더 효율적으로 자원을 사용하면서 delay를 시킬 수 있다.
private val executor = Executors.newSingleThreadExecutor{
Thread(it, "scheduler").apply { isDaemon = true }
}
suspend fun delay(timeMillis: Long) {
suspendCoroutine {continuation ->
executor.schedule({
Thread.sleep(timeMillis)
continuation.resume(Unit)
}, timeMillis, java.util.concurrent.TimeUnit.MILLISECONDS)
}
}
fun main() {
println("Droid")
//Thread.sleep(1000L)
delay(1000L)
println("Knights")
}
위의 내용들을 보고 코루틴의 사용법에 대해 알아 보았다.
이제 다음 Chapter의 “CoroutineScope in Android” 에서 이야기 될 알아두면 좋을 Keyword를 살펴보자.
1. Suspend 함수 내부에는 Continuation이 숨겨져 있어 재개가 가능하다.
아래 예시에서 getUserName함수는 continuation을 사용했고, printUserName 함수는 사용하지 않았다고 가정하자.
suspend fun getUserName(): String {
return suspendCoroutine {continuation ->
continuation.resume("DroidKnights")
}
}
suspend fun printUserName(name: String) {
println(name)
}
suspend function으로 작성된 코드를 Decompile 해보면, 함수의 마지막 파라미터로 Continuation이 추가되어 있다.
2. Continuation 객체의 타입을 지정할 수 있으며 resume을 통해 반환 되는 값은 반드시 같은 타입이다.
지금까지의 예시와 아래 예시를 보면 resume 함수에 Unit 을 인자로 굳이 넣는 것을 볼 수 있다.
또 suspendCoroutine의 타입 인자로 Unit을 사용한다. 이는 우연의 일치가 아니다.
suspendCoroutine은 호출될때 Continuation 객체로 반환될 값의 타입을 지정할 수 있으며 resume을 통해 반환 되는 값은 반드시 지정된 타입과 같은 타입이어야 한다.
suspend fun main() {
println("Before")
suspendCoroutine<Unit> { continuation ->
thread {
println("Suspended")
Thread.sleep(1000)
continuation.resume(Unit)
println("Resumed")
}
}
println("After")
}
//Before
//Suspended
//(1초뒤)
//After
//Resumed
자 이제 우리가 Android에서 사용한 Coroutine이 잘 사용하고 있는지 살펴 보도록 하자.
CoroutineScope in Android
1. How to use Coroutine in Retrofit
아래 코드는 Retrofit의 간단한 예제이다.
interface UserApi {
@GET("user/{userId}")
suspend fun getUser(@Path("userId") userId: Long): User
}
여기서 궁금해야 할 점은 세가지이다.
- Continuation을 사용한 적이 없는데 어떻게 중단 → 재개가 이루어질까?
- Retrofit의 API 반환은 어떻게 이루어 질까?
- suspend fun 으로 Interface로 선언만 해도 코루틴이 왜 동작하며 그냥 function으로 정의한 것과 어떻게 구분할까?
아래는 Retrofit v2.6.0 이후의 내부 코드로 구성되어 있다.
HttpServiceMethod.java & KotlinException.kr
Retrofit 내부의 HttpServiceMethod.java는 인터페이스만 넘기고 구현체는 retrofit에서 생성 해주는 클래스이다.
이 코드를 보면 위에서 isKotlinSuspendFunction이 true일 경우 SuspendForBody라는 객체를 만들어서 SuspendFunction에 맞게끔 수행하게 한다.
그 내부 코드를 살펴보면 KotlinException.kr 내에서 Call.await() 이라는 함수를 만들어서 enqueue를 통해 비동기로 받은 값을 중단점을 잡아서 코루틴으로 감싸준 형태로 구현되어 있다.
아래 코드에서 보면 suspendCancellableCoroutine이라는 함수로 감싸주고 취소가 발생할 경우 call을 cancel해준다.
네트워크에서 객체를 받았을 경우 continuation.resumeWithException을 호출하여 해당 네트워크에서 받은 객체를 반환해준다.
이를 통해 알 수 있는 점은
- Retrofit 내부에서 Continuation을 사용하여 중단과 재개를 사용하고 있기에 우리는 중단, 재개 시점을 신경쓸 필요 없으며 Continuation 객체를 만들지 않아도 된다.
- Retrofit 내부에서 Continuation 의 반환 타입을 지정해 주기 때문에 쉽게 원하는 응답값을 지정 가능하다.
RequestFactory.java
다음은 RequestFactory.java 내부 코드이다.
아래와 같이 리플렉션을 통해서 인터페이스에 정의된 함수에 파라미터를 검사를 해서 파라미터 타입중에 Continuation이라는 타입이 있으면 suspendFunction이라고 판별한다.
이로서 알 수 있는 점은 suspend fun 으로 Interface로 선언만 해도 코루틴을 실행시켜준다.
2. 반환 값을 Response 객체나 Call 객체로 받아야 할까?
또 우리는 가끔 아래와 같은 형태처럼 Reponse<T> 혹은 Call<T>를 감싸기도 한다. 과연 이 행위도 맞을까?
interface MyService {
@GET("user/{id}")
suspend fun getUser(@Path("id") id: Int): Response<User>
}
아래를 보면 Retrofit v2.6.0 suspend 함수를 사용하면 코루틴을 사용하여 결과를 직접 반환 하므로, Call을 사용하여 결과를 받아올 필요가 없다.
3. Retrofit과 Coroutine 간의 ViewModel에선 Dispacher.IO로 전환해야 할까?
우리는 서버 통신 및 내부 저장은 Dispacher.IO 안에서 실행 한 후 Main으로 넘겨줘 ui에 반영해준다는 말을 지겹게 들었다.
이유는 Dispacher.IO는 필요에 따라 추가적으로 스레드를 더 생성하거나 줄일 수 있으며 최대 64개까지 생성이 가능하기 때문이라고 한다. 또한 Main을 점유하고 있으면 UI에서 작업하기에 ANR이 발생 할 수 있다고 한다.
그래서 구글링하여 여러 블로그의 예시 코드를 보면 ViewModel.scope 안에 Dispacher.IO를 감싼 형태거나 먼저 Dispacher.IO 에서 코루틴 스코프를 설정하여 데이터를 가져온 후 내부에서 Dispacher.Main 으로 감싼 형태를 볼 수 있다.
결론적으로 위의 행동은 아무런 영향을 주지 못한다.
이유는 Retrofit 라이브러리는 네트워크 요청을 실행하는 동안 내부적으로 자체적인 비동기 처리를 수행하기 때문이다.
아래 코드는 retrofit의 enqueue가 OkHttp의 enqueue() 메소드를 사용하고 그 OkHttp의 enqueue() 의 내부 코드는 간략하게 아래와 같은데 client.dispatcher().enqueue(new AsyncCall(responseCallback)) 에서 네트워크 요청을 비동기적으로 처리하기 위해 AsyncCall 클래스를 사용하는데 이 클래스는 Runnable 인터페이스를 구현하므로, 별도의 스레드에서 실행될 수 있다.
@Override
public void enqueue(Callback responseCallback) {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
client.dispatcher().enqueue(new AsyncCall(responseCallback));
}
아래는 AsyncCall 의 내부 코드중 promoteAndExecute() 함수이다.
promoteAndExecute() 메소드는 Dispatcher의 실행 대기 큐(readyAsyncCalls)에서 AsyncCall을 가져와 실행 큐(runningAsyncCalls)로 옮긴 후, AsyncCall의 executeOn() 메소드를 호출한다. executeOn() 메소드는 AsyncCall을 인자로 받는 dispatcher.executorService().execute()를 호출하여 별도의 스레드에서 네트워크 요청을 처리한다.
따라서, promoteAndExecute() 메소드는 AsyncCall을 실행 큐로 옮기고, executeOn() 메소드를 통해 별도의 스레드에서 네트워크 요청을 처리하도록 한다. 이는 OkHttp가 네트워크 요청을 비동기적으로 처리하는 방식이다.
그래서 정리하면 Coroutine을 이용할 경우 내부적으로 enqueue를 진행해주고 있으며, enqueue는 retrofit에서 내부적으로 비동기 처리를 해주고 있으니 Dispatchers.IO를 사용하지 않아도 된다.
여기까지 라이브러리와 코루틴을 이해할 만한 내용을 다뤘다. 다음은 노하우에 대해 살펴보자.
async()를 사용하면서 await()를 즉시 호출하지 말자
여러가지 함수를 병렬적으로 수행을 하면서 비동기적인 작업을 동기화 하는 작업을 할때는 async()가 효율적이다.
그러나 async를 사용하면서 await()를 즉시 호출하는 것은 아무런 이점이 없다.
차라리 그냥 suspend function을 사용하자.
Don't
suspend fun getUser(): User = coroutineScope {
val user = async { api.getUser() }.await()
user.toDomain()
}
Do
suspend fun getUser(): User {
val user = api.getUser()
return user.toDomain()
}
withContext()의 인자값을 통해 CoroutineContext 를 override 하는 것이 아니라면coroutineScope{}를 사용하자
withContext()는 coroutineScope{ } 와 차이는 CoroutionContext를 override하는것이 딱하나의 차이점만 있다.
그러니 CoroutionContext를 재정의 할때만 withContext() 사용하고 이외에는 coroutineScope{ } 것이 좋다.
suspend fun exampleWithContext() = withContext(Dispatchers.IO) {
// 이 블록은 Dispatchers.IO에서 실행됩니다.
}
suspend fun exampleWithScope() = coroutineScope {
// 이 블록은 현재의 CoroutineContext에서 실행됩니다.
}
비동기가 아닌, 동기로 처리해야 하는 로직은 Dispatchers.Main 보다는 Dispatchers.Main.immediate 를 사용하자
Dispatcher.Main은 핸들러를 통해 큐에 쌓는 작업이기 때문에 즉시 수행될것이라는 보장이 없다. 그러나 Dispatcher.Main.immediate는 즉시 실행하는 것이고 최적화된 버젼으로 불필요한 coroutine redispatching을 피한다.
일반적으로 UI를 업데이트 할때에는 Dispacher.Main 보다는 Dispacher.Main.immediate가 더 좋다.
이유는 Main으로 설정하여 큐에 쌓았다가 다른 작업이 오래 걸려 UI 작업이 늦어져 ANR이 발생할 수 있기 때문에 Dispacher.Main.immediate이 좋다.
참고로 우리가 자주 사용하는 lifecycleScope 는 내부적으로 Dispathchers.Main.immediate 을 사용하고 있다.
lifecycleScope.launch {
// 코루틴 코드
}
동작이 무거운 non-suspend 함수의 경우 yield() 를 활용하자
아래의 코드의 예시는 하나의 덩어리인 함수가 중간에 취소가 되는 경우 yield()함수를 심어줌으로서 중단점을 만들어줬다.
이처럼 yield()는 suspension point를 설정해주는 용도로 활용가능하며, non-suspend function도 cancellation과 coroutine redispatching의 도움을 받을 수 있게 할 수 있다.
suspend fun getUser() = heavyFunction() = withContext(Dispatchers.Default) {
nonSuspendHeavyFunction1()
yield()
nonSuspendHeavyFunction2()
yield()
nonSuspendHeavyFunction3()
}
코루틴 취소가 발생 했을 때에도, 해제 해줘야 하는 자원(resource)이 있는 경우 finally black을 활용해라
기본적으로 cancel의 원리가 Exception을 통한 cancel 관리로 이루어져 있다. cancel에 의해 close해줘야 하는 작업이나 해제해줘야 하는 작업이 있을 경우 try 블럭을 만들고 finally 문들 만들어 close()같은 작업을 해주게 된다면 아래 예시 코드처럼 반드시 에러가 발생하지 않아도 coroutine 취소가 발생했을 경우에도 finally 문이 실행시켜 자원의 낭비를 줄이자
suspend fun readFile() = coroutineScope {
val job = launch {
val file = openFile("example.txt")
try {
} finally {
}
}
delay(1000)
job.cancelAndJoin()
}
Experiment 5 Case Code
ResultUseCase.kr
abstract class ResultUseCase<in Params, R> {
operator fun invoke(params: Params?): Flow<ResultStatus<R>> = flow {
emit(ResultStatus.Loading)
emit(doWork(params))
}.catch { throwable ->
emit(ResultStatus.Error(throwable))
}
protected abstract suspend fun doWork(params: Params?): ResultStatus<R>
}
fun <T> Flow<ResultStatus<T>>.doWork(
scope: CoroutineScope,
isLoading: () -> Unit,
isSuccess: (T) -> Unit,
isError: (Throwable) -> Unit,
) {
scope.launch {
collect { status ->
when (status) {
is ResultStatus.Loading -> {
isLoading()
}
is ResultStatus.Success<T> -> {
isSuccess(status.data)
}
is ResultStatus.Error -> {
isError(status.throwable)
}
}
}
}
}
아래는 예시 ViewModel이다.
5가지의 상태를 보여주고 있는데 상황별 작성해 볼 수 있는 코드를 기입해 봤다.
@RequiresApi(Build.VERSION_CODES.O)
@HiltViewModel
class CoroutineTestViewModel @Inject constructor(
private val getCoroutineTest0UseCase: GetCoroutineTest0UseCase,
private val getCoroutineTest1UseCase: GetCoroutineTest1UseCase,
private val getCoroutineTest2GetUserUseCase: GetCoroutineTest2GetUserUseCase,
private val getCoroutineTest2GetNewsUseCase: GetCoroutineTest2GetNewsUseCase,
private val getCoroutineTest3UseCase: GetCoroutineTest3UseCase,
private val getCoroutineTest4ExceptionUseCase: GetCoroutineTest4ExceptionUseCase,
) : ViewModel() {
val testItem: MutableLiveData<CoroutineTest> = MutableLiveData()
private val formatter: DateTimeFormatter = DateTimeFormatter.ofPattern("HH:mm:ss")
init {
//test0()
// test1()
// test2()
// test3()
test4()
}
/**
* 2가지의 API를 순차적으로 호출 하고 그 결과를 합쳐서 보여준다.
* API를 받은 이후에 다음 API를 호출하기 때문에 시간이 낭비 된다.
*/
private fun test0() {
testItem.value = CoroutineTest("시작", LocalDateTime.now().format(formatter))
getCoroutineTest0UseCase.invoke(Unit).doWork(
viewModelScope,
isLoading = { },
isSuccess = {
it.forEach { i -> testItem.value = i }
testItem.value = CoroutineTest("끝", LocalDateTime.now().format(formatter))
},
isError = { },
)
}
/**
* 2가지의 API를 동시에 호출하고 그 결과를 합쳐서 보여준다.
* 동시에 호출 되기 때문에 시간이 낭비 되지 않는다.
*/
private fun test1() {
testItem.value = CoroutineTest("시작", LocalDateTime.now().format(formatter))
getCoroutineTest1UseCase.invoke(Unit).doWork(
viewModelScope,
isLoading = { },
isSuccess = {
it.forEach { i -> testItem.value = i }
testItem.value = CoroutineTest("끝", LocalDateTime.now().format(formatter))
},
isError = { },
)
}
/**
* API를 동시에 호출하고 그 결과를 보여준다.
* 와면이 중단된다면 그 이후의 API는 호출 되지 않으므로 불필요한 API 호출을 막을 수 있다.
* (getUser의 시간을 1초로 바꿔야 함)
*/
private fun test2() {
testItem.value = CoroutineTest("시작", LocalDateTime.now().format(formatter))
getUser()
getNews()
}
/**
* 2가지의 API를 순차적으로 호출 하고 그 결과를 합쳐서 보여준다.
* 순서를 보장 받기 때문에 순서가 중요한 경우 사용한다.
*/
private fun test3() {
testItem.value = CoroutineTest("시작", LocalDateTime.now().format(formatter))
getCoroutineTest3UseCase.invoke(Unit).doWork(
viewModelScope,
isLoading = { },
isSuccess = {
testItem.value = it
testItem.value = CoroutineTest("끝", LocalDateTime.now().format(formatter))
},
isError = { },
)
}
/**
* API 호출 중 Exception이 발생하면 error를 호출한다.
* Exception을 핸들링 하기 용이하다.
*/
private fun test4() {
testItem.value = CoroutineTest("시작", LocalDateTime.now().format(formatter))
getCoroutineTest4ExceptionUseCase.invoke(Unit).doWork(
viewModelScope,
isLoading = { },
isSuccess = {
it.forEach { i -> testItem.value = i }
testItem.value = CoroutineTest("끝", LocalDateTime.now().format(formatter))
},
isError = {
Log.d("qweqwe", "it : " + it)
},
)
}
private fun getUser() {
getCoroutineTest2GetUserUseCase.invoke(Unit).doWork(
viewModelScope,
isLoading = { },
isSuccess = { testItem.value = it },
isError = { },
)
}
private fun getNews() {
getCoroutineTest2GetNewsUseCase.invoke(Unit).doWork(
viewModelScope,
isLoading = { },
isSuccess = { testItem.value = it },
isError = { },
)
}
}
class GetCoroutineTest0UseCase(
private val coroutineTestRepository: CoroutineTestRepository,
) : ResultUseCase<Unit, List<CoroutineTest>>() {
override suspend fun doWork(params: Unit?): ResultStatus<List<CoroutineTest>> {
return try {
val result: MutableList<CoroutineTest> = mutableListOf()
result.add(coroutineTestRepository.getUser())
result.add(coroutineTestRepository.getNews())
ResultStatus.Success(result)
} catch (throwable: Throwable) {
ResultStatus.Error(throwable)
}
}
}
class GetCoroutineTest1UseCase(
private val coroutineTestRepository: CoroutineTestRepository,
) : ResultUseCase<Unit, List<CoroutineTest>>() {
override suspend fun doWork(params: Unit?): ResultStatus<List<CoroutineTest>> {
return try {
coroutineScope {
val result: MutableList<CoroutineTest> = mutableListOf()
val user = async { coroutineTestRepository.getUser() }
val news = async { coroutineTestRepository.getNews() }
result.add(news.await())
result.add(user.await())
ResultStatus.Success(result)
}
} catch (throwable: Throwable) {
ResultStatus.Error(throwable)
}
}
}
class GetCoroutineTest2GetUserUseCase(
private val coroutineTestRepository: CoroutineTestRepository,
) : ResultUseCase<Unit, CoroutineTest>() {
override suspend fun doWork(params: Unit?): ResultStatus<CoroutineTest> {
return try {
ResultStatus.Success(coroutineTestRepository.getUser())
} catch (throwable: Throwable) {
ResultStatus.Error(throwable)
}
}
}
class GetCoroutineTest2GetNewsUseCase(
private val coroutineTestRepository: CoroutineTestRepository,
) : ResultUseCase<Unit, CoroutineTest>() {
override suspend fun doWork(params: Unit?): ResultStatus<CoroutineTest> {
return try {
ResultStatus.Success(coroutineTestRepository.getNews())
} catch (throwable: Throwable) {
ResultStatus.Error(throwable)
}
}
}
class GetCoroutineTest3UseCase(
private val coroutineTestRepository: CoroutineTestRepository,
) : ResultUseCase<Unit, CoroutineTest>() {
override suspend fun doWork(params: Unit?): ResultStatus<CoroutineTest> {
return try {
coroutineScope {
val user = async { coroutineTestRepository.getUserName() }
val userNews = coroutineTestRepository.getUserNews(user.await())
ResultStatus.Success(userNews)
}
} catch (throwable: Throwable) {
ResultStatus.Error(throwable)
}
}
}
class GetCoroutineTest4ExceptionUseCase(
private val coroutineTestRepository: CoroutineTestRepository,
) : ResultUseCase<Unit, List<CoroutineTest>>() {
override suspend fun doWork(params: Unit?): ResultStatus<List<CoroutineTest>> {
return try {
val result: MutableList<CoroutineTest> = mutableListOf()
result.add(coroutineTestRepository.getUser())
result.add(coroutineTestRepository.getCoroutineException())
ResultStatus.Success(result)
} catch (throwable: Throwable) {
ResultStatus.Error(throwable)
}
}
}
'도서 > 코틀린 코루틴' 카테고리의 다른 글
2장 코루틴 라이브러리 - CoroutineContext (1) | 2024.02.03 |
---|---|
2장 코루틴 라이브러리 - Coroutine Buidler (0) | 2024.02.03 |
1장 코틀린 코루틴 이해하기 - 중단은 어떻게 작동할까? (0) | 2024.01.09 |
1장 코틀린 코루틴 이해하기 - 코틀린 코루틴을 배워야 하는 이유 (0) | 2024.01.08 |
Retrofit 과 Coroutine 간의 Viewmodel에선 Dispacher.IO로 전환해야 할까? (1) | 2023.11.30 |