Android/Coroutine

[Kotlin Coroutine][init] 1.코틀린 코루틴? 왜 꼭 코루틴을 사용해야하나요?

네모메모 2024. 2. 17. 11:54
반응형

 

 

 

 

 

 

 

 

 

* 작성 내용 : 코틀린 코루틴이란 무엇인지? 어떻게 작동하는지??

 

 

 


 

 

 

 

Kotlin + Coroutines 을 같이 사용하는 장점

 

 


 

안드로이드에서 코루틴을 왜 사용해야하는지?

- 안드로이드에서는 메인스레드 단 하나만 뷰를 다룰 수 있음 -> 이 쓰레드가 블로킹되면 앱이 ANR 발생하며 죽음

"안드로이드에서는 하나의 앱에서 뷰를 다루는 스레드가 '단 하나'만 존재하기 때문에 Main 스레드를 블로킹되서는 안된다."

 

- 하지만, 뷰를 다루는 작업은 엄청 많음 -> 따라서, 메인스레드를 블로킹할 위험이 있는 로직 작업들은 별도 처리해야함!!
   ㄴ> 앱 로직을 수행할 때 가장 자주 진행되는 과정

  • API, 데이터베이스 등과 같은 소스의 데이터 가져오기
  • 데이터 가공하기
  • 그 데이터로 뷰에 보여주는 등의 작업 수행하기

 

 

 

 


 

 

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)
    }
  }
}

ㄴ> [다른 스레드 사용 시 문제점]

  1. 스레드가 실행되었을때 멈출 수 있는 방법이 없어 메모리 누수가 이어 질 수 있다.
    ㄴ> ex) 사용자의 빠른 화면이동, 연타 등으로 반복 접속, 앱이 background로 내려갈 경우 -> 뷰는 이미 사라졌는데 작업이 실행되면 exception이나 예상 외 결과가 발생할 수 있어 위험하다.
  2. 스레드를 너무 많이 생성하면 비용이 많이 든다.
  3. 스레드를 자주 전환 화면 복잡도가 증가하며 관리하기 어렵다.
  4. 코드가 쓸데없이 길어지고 이해하기 어려워진다.

 

 

 

#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)
            }
        }
    }
}

위 코드는 다음과 같은 이유로 완벽한 해결책이 될 수 없다.

      1. 뉴스를 얻어오는 작업과 사용자 데이터를 얻어오는 작업은 각각 병렬로 처리할 수 있지만, 콜백으로 처리하기 매우 어렵다.
      2. 취소할 수 있도록 구현하려면 더 어렵다.
      3. 들여쓰기가 많아져 코드가 읽기 복잡해진다.
         >> 이는 '콜백지옥(callback hell)'으로 node.js쪽에서 이미 유명한 문제
      4. 콜백을 사용하면 작업의 순서를 다루기 어렵다. 
        이런 식이면 뉴스를 가져온 이후 처리되는 모든 앱의 작업은 이제 모두 블록 안에 위치해야합니다.👇
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

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

반응형