코틀린 코루틴 (1장 정리)

윤성현·2024년 12월 11일

코틀린 코루틴

목록 보기
1/11
post-thumbnail

1장. 코틀린 코루틴을 배워야 하는 이유

RxJava, Reactor, 멀티 스레드, 콜백함수 등 이미 다양한 비동기적 연산을 위한 방법이 있는데 코틀린 코루틴을 배워야할까 🤔

서론

  • 코루틴의 개념은 1963년에 처음 제시되었다. 🕰
    (코루틴은 하나의 개념에서 파생된 것으로 코틀린에 종속된 것이 아니다)
  • 하지만 현업에서 사용될 수 있도록 구현되기까지 수십 년이 걸렸다.
    (2009년 Go는 가장 먼저 코루틴을 상용화 도입하였다 🚀)
  • 코틀린 코루틴은 도입한다고 해서 기존 코드 구조를 광범위하게 고칠 필요가 없다. 🛠

📱 안드로이드(FE 플랫폼)에서의 코루틴 사용

⚠️ 메인 스레드 블로킹 문제

  • 안드로이드에서는 하나의 앱에서 뷰를 다루는 스레드(Main Thread)가 단 하나만 존재
  • 메인 스레드를 블로킹하면 안 됨
fun onCreate() {
	val news = getNewsFromApi()
    val sortedNews = news.sortedByDescending { it.publishedAt }
    view.showNews(sortedNews)
}
  • 문제점
    • onCreate 함수가 메인 스레드에서 실행 → getNewsFromApi 함수가 스레드를 블로킹 → 앱 크래시 💥
    • getNewsFromApi 함수를 다른 스레드에서 실행 → showNews 호출시 정보가 없어 메인스레드에서도 마찬가지로 크래시 💥

🔄 스레드 전환

  • 가장 쉬운 해결 방법: 블로킹이 가능한 스레드를 사용한 후 메인 스레드로 전환
fun onCreate() {
    val news = getNewsFromApi()
    val sortedNews = news.sortedByDescending { it.publishedAt }
    runOnUiThread {
        view.showNews(sortedNews)
    }
}
  • 문제점
    • 스레드 멈춤 불가 → 메모리 누수 위험 🧹
    • 스레드 생성 비용 증가
      → 약 1MB 메모리가 필요하며, 컨텍스트 스위칭이 CPU 자원을 소모 🖥
    • 복잡성 증가 → 스레드가 많아질수록 관리가 어려워짐 💼
    • 코드 가독성 저하 → 보일러 플레이트 코드 증가 📄

🔁 콜백

  • 콜백의 기본적인 방법은 함수를 논블로킹으로 만들고, 함수의 작업이 끝났을 때 호출될 콜백 함수를 넘겨주는 것
    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)
    			}
    		}
    	}
    }
    • 뉴스를 얻어오는 작업과 사용자 데이터를 얻어오는 작업은 병렬로 처리할 수 있지만, 현재의 콜백 구조로는 두 작업을 동시에 처리할 수 없음 (콜백으로 해결하기 매우 어려움)
    • 취소할 수 있도록 구현하려면 많은 노력이 필요
    • 들여쓰기가 많아질수록 코드는 읽기 어려워짐 (A.K.A. 콜백 지옥 🔥)
  • 콜백을 사용하면 작업의 순서를 다루기 어려워짐 🌀
    // As-Is (Not-Working)
    fun onCreate() {
    	showProgressBar()
    	showNews()
    	hideProgressBar() // wrong point
    }
    // To-Be
    fun onCreate() {
    	showProgressBar()
    	showNews() {
    		hideProgressBar()
    	}
    }
    • 프로그레스 바는 뉴스를 보여주는 작업을 시작하고 곧바로 사라져야하기 때문에 showNews에 콜백 함수를 만들어야 함

🌊 RxJava와 리액티브 스트림

  • RxJava, Reactor와 같은 리액티브 스트림을 사용하면 데이터 스트림 내에서 일어나는 모든 연산을 시작, 처리, 관찰할 수 있음

  • 리액티브 스트림은 스레드 전환과 동시성 처리를 지원 → 애플리케이션 내의 연산을 병렬 처리하는데 사용됨

    fun onCreate() {
    	disposables += getNewsFromApi() {
    		.subscribeOn(Schedulers.io())
    		.observeOn(AndroidSchedulers.mainThread())
    		.map { news ->
    			news.sortedByDescending { it.publishedAt }
    		}
    		.subscribe { sortedNews ->
    			view.showNews(sortedNews)
    		}
    }

    위 예제에서 disposables는 사용자가 스크린을 빠져나갈 경우 스트림을 취소하기 위해 필요

  • RxJava를 사용한 방법이 콜백을 사용한 것보다 훨씬 더 좋은 방법

    • ✅ 메모리 누수 없음
    • ✅ 취소 가능
    • ✅ 스레드를 적절하게 사용
  • 하지만 단점도 존재

    • 구현하기에 아주 복잡함 😵
    • 이상적인 코드(초기 코드)와 비교하면 완전히 다른 형태의 코드
    • subscribeOn, observeOn, map, subscribe와 같은 함수들을 배워야 함 📘
    • 취소하는 작업을 명시적으로 표시해야 함
    • 객체를 반환하는 함수들은 Observable 이나 Single클래스로 래핑해야 함
fun getNewsFromApi(): Single<List<News>>
  • 데이터를 보여주기 전에 세 개의 엔드포인트를 호출해야 한다는 것도 문제
    (endpoint, 서비스에서 다른 서비스에 요청을 보내는 지점)
fun showNews() {
    disposables += Observable.zip(
    	getConfigFromApi().flatMap { getNewsFromApi(it) },
    	getUserFromApi(),
    	Function2 { news: List<News>, config: Config ->
    		Pair(news, config)
    	})
    	.subscribeOn(Schedulers.io())
    	.observeOn(AndroidSchedulers.mainThread())
    	.subscribe { (news, config) ->
    		view.showNews(news, config)
    	}
}
  • 위 코드는 동시성 처리도 되어 있고 메모리 누수도 없지만, RxJava 함수를 사용해야 하고 값을 Pair로 묶고 분리도 해야함
  • 적절한 구현 방법이지만 너무 복잡하다는 단점이 있음

코틀린 코루틴의 사용

  • 핵심 기능 : 코루틴을 특정 지점에서 멈추고 이후에 재개할 수 있음
    • 작성한 코드를 메인 스레드에서 실행하고 API에서 데이터를 얻어올 때 잠깐 중단시킬 수 있음
    • 코루틴을 중단시킬 때 스레드는 블로킹되지 않고 다른 코루틴을 실행하는 등의 작업이 가능 🌐
    • 데이터가 준비되면 코루틴은 메인 스레드에서 대기하고 있다가 메인스레드가 준비되면 멈춘 지점에서 다시 작업을 시작
val scope = CoroutineScope(Dispatchers.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)
}
  • 두 함수가 한 스레드 내에서 넘나들며 실행될 수 있는 이유는 스레드가 블로킹되는 것이 아니라 코루틴이 중단되기 때문
  • updateNews 함수가 네트워크 응답을 기다리는 동안, 메인 스레드는 updateProfile 함수가 사용
  • 이때 getUserData 호출은 사용자의 데이터가 캐싱되어 있기 때문에 중단되지 않는다고 가정
  • updateProfile은 메인 스레드 내에서 멈추지 않고 실행됨
  • 데이터 전송이 완료되면 메인스레드는 updateNews를 처리하는 코루틴에 의해 getNewsFromApi 이후 작업을 수행

코루틴은 중단했다가 다시 실행할 수 있는 컴포넌트(재사용 가능한 독립된 모듈)라고 할 수 있다.

  • 예시 (코틀린 코루틴을 사용한 뉴스 처리 작업)
fun onCreate() {
    viewModelScope.launch {
    	val news = getNewsFromApi()
    	val sortedNews = news.sortedByDescending { it.publishedAt }
    	view.showNews(sortedNews)
    }
}

viewModelScope는 현재 안드로이드에서 가장 흔하게 사용하는 스코프. viewModelScope 대신 사용할 스코프를 지정할 수 있음.

  • 예시 코드는 시작할 때 봤던 코드와 거의 동일함
  • 코드가 메인스레드에서 실행되지만 스레드를 블로킹하지는 않음
  • 코루틴의 중단은 기다릴 때 코루틴을 잠시 멈추는 방식으로 작동
  • 코루틴이 멈춰 있는 동안, 메인 스레드는 UI 작업 등 다른 작업을 할 수 있음
  • 데이터가 준비되면 코루틴은 다시 메인 스레드를 할당받아 이전에 멈춘 지점부터 다시 시작됨
  • 예시 (세 개의 엔드포인트를 호출해야하는 문제)
fun showNews() {
    viewModelScope.launch {
    	val config = getConfigFromApi()
    	val news = getNewsFromApi(config)
    	val user = getUserFromApi()
    	view.showNews(user, news)
    }
}
  • 작동 방식이 효율적이지 않음
  • 호출이 순차적으로 일어나기 때문에, 각 호출이 1초씩 걸린다면 전체 함수는 3초가 걸림
  • API를 병렬로 호출했다면 2초만에 작업을 끝낼 수 있음
  • 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())
	}
}
  • 위 코드는 효율적으로 작동하며 메모리 누수가 일어나지 않음 ✅
  • 코드가 간단함 🧾

백엔드에서의 코루틴 사용

  • 백엔드에서 코루틴을 사용하면 코드가 간결해짐
  • RxJava와 달리 코드에 큰 변화가 없음
  • 코루틴으로 바꾸는 대부분의 환경에서 suspend 제어자(modifier)를 추가하면 됨

코루틴을 사용하는 가장 중요한 이유는 스레드를 사용하는 비용이 크기 때문. 스레드는 명시적으로 생성해야 하고, 유지되어야 하며, 스레드를 위한 메모리 또한 할당되어야 함.

  • 코루틴을 시작하는 비용은 스레드와 비교가 되지 않을 정도로 저렴하여, 인지할 수 없을 정도

요약

코틀린 코루틴은 동시성 프로그래밍을 최대한 쉽게 구현할 수 있도록 도와줌

1. 코루틴의 배경과 중요성

  • 코루틴은 1963년에 개념화되었으며, 2009년 Go 언어에서 처음 상용화되었다.
  • 코틀린 코루틴은 기존 코드 구조를 크게 변경하지 않고 비동기 작업을 간소화할 수 있다.

2. 안드로이드에서의 비동기 처리 방법

  • 기존 방법의 한계
    • 스레드 전환: 메모리 누수, 높은 비용, 관리 복잡성 발생.
    • 콜백: 취소 어려움, 코드 복잡성 증가(콜백 지옥).
    • RxJava: 스레드 전환과 동시성 지원. 하지만 복잡한 문법과 리소스 관리가 필요하다.
  • 코루틴의 장점
    • 코루틴은 스레드를 블로킹하지 않고 중단 및 재개 가능.
    • 간단한 문법으로 비동기 작업 수행.
    • 작업을 취소하거나 스레드 자원을 효율적으로 사용.
    • 병렬 처리가 간단하며 메모리 누수가 없다.

3. 코드 비교

  • 코루틴을 사용한 코드는 동기적 코드처럼 간결하지만 비동기로 동작.
  • asyncawait를 활용해 작업을 병렬로 처리 가능.
  • 결과적으로 효율적이고 가독성이 높은 코드를 작성할 수 있다.

4. 백엔드에서의 활용

  • 백엔드에서도 코루틴은 코드 단순화에 기여.
  • suspend 키워드만 추가하면 비동기 작업을 쉽게 전환 가능.
  • 스레드와 달리 코루틴은 비용이 적고, 경량화된 작업 실행이 가능.

5. 결론

  • 코루틴은 스레드의 높은 비용을 해결하고 코드 복잡성을 낮추는 강력한 도구.
  • RxJava, 콜백, 스레드 전환의 단점을 극복하며 안드로이드 및 백엔드 개발에서 효율성을 극대화한다.

0개의 댓글