코틀린 코루틴을 배워야 하는 이유
예시를 들어보자.
아래 예시는 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)
}
}
}
그러나 이러한 방식에도 문제가 있다.
예를 들면 뷰를 열었다가 재빨리 닫으면 뷰가 잠깐 열려있는동안 스레드가 다수 생성되어 생성된 스레드를 제거하지 않으면 불필요한 작업을 게속 수행하다 뷰를 수정하려고 시도할 것이다. 이렇게 되면 예상하기 어려운 결과가 발생할 수 있다.
이러한 문제점은 아래와 같이 정리할 수 있다.
- 스레드가 실행되었을때 멈출 수 있는 방법이 없어 메모리 누수가 이어 질 수 있다.
- 스레드를 너무 많이 생성하면 비용이 많이 든다.
- 스레드를 자주 전환 화면 복잡도가 증가하며 관리하기 어렵다.
- 코드가 쓸데없이 길어지고 이해하기 어려워진다.
위의 문제를 해결하기 위해 더 나은 방법을 생각해보자.
다음은 콜백을 사용하여 논 블로킹을 을 만들고 함수작업이 끝났을때 호출될 콜백 함수를 넘겨주는 것이다.
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)
}
}
그러나 이러한 예시는 복잡하다는 단점이 있다.
subscribeOn, observeOn, map, subscribe와 같은 함수들은 RxJava를 사용하기 위해 배워야 한다.
또한 취소하는 작업 또한 명시적으로 표시해야 한다.
그렇다면 우리의 코루틴은 어떤가?
코루틴의 핵심 기능은 코루틴을 특점 지점에서 멈추고 이후에 재개 할 수 있다는 것이다.
이러한 핵심은 코루틴을 사용하면 우리가 짠 코드를 메인 스레이스에서 실행하고 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)
}
위의 코드를 보면 updateedNews와 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)
}
}
}
알아두면 좋은 코드
아래 두개의 함수는 다른 방식이다.
1번은 getUserAPI() 함수와 getPeedApi() 함수를 비동기적으로 불러 user 데이터와 peed 데이터를 따로 불러오게 되는데
2번은 getUserAPI()를 기다린 후에 getPeedApi()를 불러온다.
suspend fun fetchData() {
coroutineScope {
val userDeferred = async { getUserApi() }
val peedDeferred = async { getPeedApi() }
val user = userDeferred.await()
val peed = peedDeferred.await()
// 이후 user와 peed를 이용한 로직을 여기에 작성하실 수 있습니다.
}
}
suspend fun fetchData() {
coroutineScope {
val userDeferred = async { getUserApi() }.await()
val peedDeferred = async { getPeedApi() }.await()
val user = userDeferred
val peed = peedDeferred
}
}
결론
위의 예시를 보며 코루틴을 배울 필요가 있다고 확신을 가졌을 것이다.
코틀린 코루틴은 단순한 라이브러리가 아니며, 최신 도구를 사용하여 동시성 프로그래밍을 최대한 쉽게 구현할 수 있도록 도와준다.
'도서 > 코틀린 코루틴' 카테고리의 다른 글
코틀린 코루틴 이해하기 + 코루틴 실전 대입하기 (0) | 2024.01.23 |
---|---|
1장 코틀린 코루틴 이해하기 - 중단은 어떻게 작동할까? (0) | 2024.01.09 |
Retrofit 과 Coroutine 간의 Viewmodel에선 Dispacher.IO로 전환해야 할까? (1) | 2023.11.30 |
[DroidKnights 2023] 김준비 - Coroutine Deep Dive - Android 실전편 (0) | 2023.11.14 |
Coroutine, Thread 와의 차이와 그 특징 (0) | 2023.07.17 |