* 작성 내용 : 코틀린 코루틴이란 무엇인지? 어떻게 작동하는지??
Kotlin + Coroutines 을 같이 사용하는 장점
- 멀티플랫폼 작동 가능 (JVM, JS, iOS 등의 모듈들에서 가능)
ㄴ> 이건 코틀린 멀티플랫폼(kotlin multiplatform)의 영향이 큼 : kotlinlangSite, ,jetbrains androidDeveloper
ㄴ>
안드로이드에서 코루틴을 왜 사용해야하는지?
- 안드로이드에서는 메인스레드 단 하나만 뷰를 다룰 수 있음 -> 이 쓰레드가 블로킹되면 앱이 ANR 발생하며 죽음
"안드로이드에서는 하나의 앱에서 뷰를 다루는 스레드가 '단 하나'만 존재하기 때문에 Main 스레드를 블로킹되서는 안된다."
- 하지만, 뷰를 다루는 작업은 엄청 많음 -> 따라서, 메인스레드를 블로킹할 위험이 있는 로직 작업들은 별도 처리해야함!!
ㄴ> 앱 로직을 수행할 때 가장 자주 진행되는 과정
|
Example) 예를 들어보자, (앱 실행 -> 최신 뉴스를 가져오는 API 요청 -> 뉴스를 정렬 -> 화면 내 리스트에 뉴스 노출) 과정이 있다
#1) 스레드 고려없이 막 만들 경우
fun onCreate() {
val news = getNewsFromApi()
val sortedNew = news.sortedByDescending { it.publishedAt }
view.showNews(sortedNew)
}
ㄴ> 메인 스레드에서 실행되었다면 getNewsFromApi() 는 이 스레드를 block할 것이고, 앱은 ANR 발생하며 죽을 것이다.
#2) 다른 스레드를 만들어 작업 수행
fun onCreate() {
thread {
val news = getNewsFromApi()
val sortedNew = news.sortedByDescending { it.publishedAt }
runOnUiThread {
view.showNews(sortedNew)
}
}
}
ㄴ> [다른 스레드 사용 시 문제점]
|
#3) 콜백 이벤트로 처리
fun onCreate() {
getNewsFromApi { news ->
val sortedNew = news.sortedByDescending { it.publishedAt }
view.showNews(sortedNew)
}
}
ㄴ> 이 문제를 해결하는 방법 중 하나으로 함수의 작업이 끝났을 때 호출될 콜백함수를 넘겨주는 방법
ㄴ> 이 방식은 함수를 non-blocking하게 만든다. +)Blocking, Non-blocking, Sync, Async 개념
ㄴ> [단점1] 스레드를 사용하지 않지만 중간에 작업을 취소할 수 없다.
(물론 취소할 수 있는 콜백함수를 만들 수 있지만 쉬운 일이 아니다. 콜백 함수 각각에 대해 취소 할 수 있도록 구현해야 하고 취소하기 위해서는 모든 객체를 분리해야 한다. )
fun onCreate() {
startedCallbacks += getNewsFromApi { news ->
val sortedNew = news.sortedByDescending { it.publishedAt }
view.showNews(sortedNew)
}
}
ㄴ> [단점+] 3 곳에서 데이터를 얻어오는 다음과 같은 예제로 발전한다고 생각해보자.
fun showNews() {
getConfigFromApi { config ->
getnewsFromApi(config) { news ->
getUserFromApi { user ->
view.showNews(user, news)
}
}
}
}
위 코드는 다음과 같은 이유로 완벽한 해결책이 될 수 없다.
- 뉴스를 얻어오는 작업과 사용자 데이터를 얻어오는 작업은 각각 병렬로 처리할 수 있지만, 콜백으로 처리하기 매우 어렵다.
- 취소할 수 있도록 구현하려면 더 어렵다.
- 들여쓰기가 많아져 코드가 읽기 복잡해진다.
>> 이는 '콜백지옥(callback hell)'으로 node.js쪽에서 이미 유명한 문제 - 콜백을 사용하면 작업의 순서를 다루기 어렵다.
이런 식이면 뉴스를 가져온 이후 처리되는 모든 앱의 작업은 이제 모두 블록 안에 위치해야합니다.👇
fun onCreate() {
showProgressBar()
showNews {
hideProgressBar()
}
}
#4) RxJava 와 리액티브 스트림을 라이브러리를 사용
fun onCreate() {
disposables += getNewsFromApi()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.map { news ->
news.sortedByDescending { it. publishedAt }
}
.subscribe { sortedNews ->
view.showNews(sortedNews)
}
}
ㄴ> [장점1] 콜백보다 더 괜찮은 해결책이다. 메모리 누수가 없고, 작업 취소(cancel)를 할 수 있으며 스레드를 사용하는 데도 적절하다.
ㄴ> [장점2] 데이터 스트림 내에서 일어나는 모든 연산을 시작, 처리, 관찰 가능하다.
ㄴ> [장점3] 데이터 스트림이 스레드 전환과 동시성 처리를 지원하므로 앱 내의 연산을 병렬 처리하는 데 사용
ㄴ> [단점1] 복잡하다. 맨 처음 코드인 “이상적인” 코드와 비교한다면 공통점이 거의 없음을 알 수 있다.
ㄴ> [단점2] 러닝커브가 필요하다. (subscribeOn, observeOn, map, subscribe와 같은 함수들은 RxJava를 사용하기 위해 배워야 한다.)
ㄴ> [단점3] 취소하는 작업 또한 명시적으로 표시해야 한다.
ㄴㄴ> (객체를 반환하는 함수들은 Observable이나 Single클래스로 래핑(Wrapping)해야해서 더 복잡해진다.
ㄴ> [단점4] 데이터를 화면에 보여주기 전에 세 개의 엔드포인트(endpoint, 서비스에서 다른 서비스에 요청을 보내는 지점)를 호출해야해서 더 복잡해진다.
#5) 코틀린 코루틴 사용하기
코루틴이 소개하는 핵심 기능은
"특정 지점에서 코루틴을 suspend(일시 정지)하고 후에 다시 resume(재시작) 할 수 있다는 것"
- 코루틴이 중단(suspend)되었을 때 ,
ㄴ> 1) 스레드 블로킹되지 않는다.
ㄴ> 2) 뷰를 바꾸거나 다른 코루틴을 진행하는 등 또 다른 작업을 할 수 있다.
- 드문 상황이지만 코루틴 대기열이 있을 수도 있다. 기다리던 스레드를 사용할 수 있게 되면 중지된 지점부터 계속 진행한다.
fun onCreate() {
viewModelScope.launch {
val news = getNewsFromApi()
val sortedNews = news.sortedByDescending { it.publishedAt }
view.showNews(sortedNews)
}
}
ㄴ> 이 때문에 예제에서도 '메인 스레드에서 코드를 실행하다가 API 데이터를 요청했을 때 중단(suspend) 할 수 있다.'
= 데이터가 준비되면 코루틴은 메인 스레드를 대기하고 있다가, 메인스레드가 준비되면 멈춘 지점부터 다시 작업을 이어 진행한다.
ㄴ> [동작설명] 위 코드는 메인 스레드에서 실행되지만 절대 스레드를 블로킹하진 않는다.
코루틴의 중단(suspend)하는 동작 덕분에 데이터를 기다릴 필요가 있을 때 코루틴을 block 대신 중단(suspend )한다.
코루틴이 일시 중단되었을 때 메인 스레드는 다른 뷰를 그리거나 다른 API를 요청하는 등의 다른 일을 할 수 있다.
그리고 이후 데이터가 준비되면 코루틴은 재개되었을 때 스레드를 다시 받아 멈췄던 부분부터 다시 시작한다.
- 예제 응용1) 중단된 동안 다른 API를 호출하는 코루틴은 어떻게 구현할까?
아래와 같이 api를 요청하는 2개의 함수가 존재한다.
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)
}
이를 메인에서 아래와 같이 호출한다.
val scope = CoroutineScope(Dispatchers.Main)
fun onCreate() {
scope.launch { updateNews() }
scope.launch { updateProfile() }
}
이 경우 코루틴은 아래와 같이 동작합니다.
1) updateNews 함수가 네트워크 응답을 기다리고 있을 때, 메인 스레드는 updateProfile이 사용한다.
(여기서는 사용자 데이터는 이미 캐싱되었기 때문에 getUserData에서 suspend하지 않았다고 가정한다. 그러므로 작업을 완료할 수 있다.)
2) 네트워크 응답 시간이 충분하지 않아서 데이터를 받아오는 게 늦어지면 메인 스레드는 그 시간동안 사용되지 않는다(다른 함수가 사용할 수 있다).
3) 데이터를 받으면 메인 스레드를 가져와 getNewsFromApi() 바로 다음 지점부터 시작하여 updateNews를 resume(재개)한다.
- 예제 응용2) #4/단점4번처럼 '데이터를 화면에 보여주기 전에 세 개의 엔드포인트(endpoint, 서비스에서 다른 서비스에 요청을 보내는 지점)를 호출'해야하는 경우 어떻게 구현할까?
#a) 기존 Rx 방식 -> (not good, 복잡)
fun showNews() {
disposables += Observable.zip(
getCondfigFromApi()
.flatMap { getNewFromApi(it) }
.subscribeOn(Schedulers.io()),
getUserFromApi()
.subscribeOn(Schedulers.io())
) { news: News, user: User ->
Pair(news, user)
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe { (news, user) ->
view.showNews((news, user))
}
}
#b) 코루틴에서 동기적으로 모두 수행 -> (not good, 오래걸려 비효율적)
ㄴ> 동기적으로 작업되어 각 API별로 소요시간이 모두 걸림 (api들이 모두 1초씩 걸리면, 총 3초 소요)
fun showNews() {
viewModelScope.launch {
val config = getCondfigFromApi()
val news = getNewsFromApi(config)
val user = getUserFromApi()
view.showNews(user, news)
}
}
#c) 코루틴에서 비동기적으로 모두 수행 -> (good)
fun showNews() {
viewModelScope.launch {
val config = async { getCondfigFromApi() }
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)
}
}
}
출처
- [Book] 코틀린 코루틴 : 안드로이드 및 백엔드 개발자를 위한 비동기 프로그래밍 마르친 모스카와 저 / 신성열 역 | 인사이트(insight) | 2023년 11월 01일
- 콜백지옥 : https://wikidocs.net/225110