[안드로이드] Coroutine은 무엇일까?
Coroutine
Coroutine이란 무엇인가요 ?
-> Coroutine은 비동기 프로그래밍을 간편하게 처리할 수 있도록 도와주는 동시 실행 패턴입니다.
Coroutine의 특징
-> 경량성, 비동기 작업의 직관적인 표현, Coroutine Scope, 일시 중지와 같은 특징이 있습니다.
그렇다면, 왜 Coroutine을 사용하나요 ?
1. Coroutine이 비동기 프로그래밍으로, 여러 작업을 처리할 수 있게 도와주기 때문입니다 !
서버와 데이터 통신을 하거나, 파일을 읽거나 쓸 때 앱이 멈추면 사용자가 앱을 사용하는데 불편함을 겪습니다.
비동기 프로그래밍이 이러한 작업들을 백그라운드에서 실행되도록 해주기 때문에 앱을 더 원활하게 사용할 수 있게 해줍니다.
2. Coroutine의 장점
- 가독성 : 복잡한 비동기 작업을 마치 동기 코드처럼 직관적으로 작성할 수 있습니다.
- 유지보수성 : 코루틴을 사용하면 코드가 더 명확해지고, 오류를 추적하기 쉬워 코드 수정이나 기능 추가 시 문제가 발생할 가능성이 줄어듭니다.
- 메모리 효율성 : 코루틴이 스레드보다 가볍습니다. 코루틴은 경량 스레드로, 동시에 실행해도 메모리 소비가 적습니다.
* 스레드는 운영체제에서 관리되며, 코루틴은 JVM에서 관리됩니다. 때문에 비용 차이도 많이 발생합니다.
다양한 방법으로 스레드와 코루틴의 효율성을 비교할 수 있지만, Visual VM으로 두 개를 비교해보겠습니다.
간단한 예제로 힙 메모리 사용량을 측정해보겠습니다.
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
fun main() {
createThreads()
Thread.sleep(10000L)
// createCoroutines()
// Thread.sleep(10000L)
}
fun createThreads() {
val threads = mutableListOf<Thread>()
for (i in 1..10000) {
val thread = Thread {
Thread.sleep(1000L) // 1초 대기
}
threads.add(thread)
thread.start()
}
threads.forEach { it.join() }
}
fun createCoroutines() = runBlocking {
val jobs = List(10000) {
launch {
delay(1000L) // 1초 대기
}
}
jobs.forEach { it.join() }
}
Thread 함수를 실행 했을 때
Coroutine 함수를 실행 했을 때
Thread는 약 764MB, Coroutine은 761MB의 힙 메모리를 사용합니다.
약 2MB로, 엄청난 차이를 나타내지는 않지만 이게 만약 많은 양의 Thread를 사용하는 앱이라면 더 차이가 날 것으로 예상됩니다.
(환경이나, 측정 시점에 따라 더 차이가 날 것 같습니다 ~)
Coroutine 사용
- build.gradle 추가 ( 버전확인 -> https://github.com/Kotlin/kotlinx.coroutines)
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0-RC.2")
}
- Scope
<자주 사용하는 Scope>
(1). GlobalScope : 코루틴을 전역적으로 관리하기 위해 사용되는 특수한 스코프입니다.
- 애플리케이션 전체의 수명 동안 작동하는 최상위 코루틴을 시작하는데 사용됩니다.
- 리소스 및 메모리 누수 가능성이 있어 조심해야 하고, suspend 함수를 사용해서 GlobalScope를 대체할 수 있습니다.
fun loadConfiguration() {
GlobalScope.launch {
val config = fetchConfigFromServer() // 네트워크 요청
updateConfiguration(config)
}
}
(2). viewModelScope : Android의 ViewModel 라이프사이클에 맞춰 코루틴을 관리하는 스코프입니다.
- ViewModel이 소멸되면 코루틴이 취소됩니다.
- UI와 관련된 비동기 작업을 할 때 사용합니다.
class MyViewModel: ViewModel() {
init {
viewModelScope.launch {
}
}
}
(3). lifecycleScope : Android의 'Lifecycle' 객체와 결합된 코루틴 스코프입니다.
- Activity나 Fragment의 생명주기에 따라 코루틴을 자동으로 취소합니다.
- UI 업데이트와 관련된 비동기 작업에 적합합니다.
class MyFragment: Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
val params = TextViewCompat.getTextMetricsParams(textView)
val precomputedText = withContext(Dispatchers.Default) {
PrecomputedTextCompat.create(longTextContent, params)
}
TextViewCompat.setPrecomputedText(textView, precomputedText)
}
}
}
Coroutine 사용법
1. launch & async
- launch : 새로운 코루틴을 시작하고, 그 결과를 기다리지 않을 때 사용합니다. -> Job을 통해 객체를 반환
launch {
delay(1000L) // 비동기 작업 수행
println("Launch: Task completed")
}
- async : 새로운 코루틴을 시작하고, 결과를 반환받을 수 있습니다. -> Deferred<T>로 객체 반환
* await() 메서드로 결과를 기다릴 수 있습니다.
val deferred: Deferred<String> = async {
delay(1000L) // 비동기 작업 수행
"Async result"
}
2. withContext
- 코루틴의 context를 변경하여 다른 Dispatcher에서 작업을 실행할 때 사용합니다.
- I/O 작업이나, CPU 작업을 적절히 사용할 때 유용합니다.
- 종류는 Main, IO, Default, Unconfined 가 있습니다.
Coroutine 사용 시 주의할 점
-> 코루틴 사용 시, 적절한 처리를 하지 않으면 예기치 않은 동작이 발생해 자원을 낭비하기 때문에 아래의 기능들도 고려해야 합니다.
- 취소 : Job 객체를 통해 취소할 수 있습니다 -> 취소하지 않으면 메모리, 네트워크 연결 등.. 낭비됩니다.
fun main() = runBlocking {
val job: Job = launch {
}
job.cancel() // 코루틴 취소
}
- 타임아웃 : 비동기 작업이 너무 오래 걸릴 때 작업을 자동으로 취소하기 위한 방법입니다 -> 시스템이 무응답 상태에 빠지는 것을 방지합니다.
* withTimeout은 TimeoutCancellationException이 발생, withTimeoutOrNull은 null을 반환합니다.
fun main() = runBlocking {
// 시간 초과시 TimeoutCancellationException 예외가 발생
try {
val result = withTimeout(1000L) {
}
} catch (e: TimeoutCancellationException) {
println("Timeout occurred: ${e.message}")
}
// 시간 초과시 예외를 발생시키지 않음
val nullResult = withTimeoutOrNull(1000L) {
}
}
- 에러처리 : 코루틴에서 발생하는 예외를 적절하게 처리해줘야 앱의 비정상적인 종료를 막을 수 있습니다.
Android에서 코루틴 활용 예제
(간단히 !)
MemoService.kt
interface MemoService {
@GET("memo")
suspend fun fetchMemos(): List<Memo>
}
- suspend 함수를 통해 코루틴 내에서 호출될 수 있도록 해줍니다.
MainViewModel.kt
class MainViewModel(private val memoRepository: MemoRepository): ViewModel() {
private val _memos = MutableLiveData<List<Memo>>()
val memos: LiveData<List<Memo>> get() = _memos
fun fetchMemos() {
viewModelScope.launch(Dispatchers.IO) {
try {
val memoList = memoRepository.fetchMemos()
_memos.postValue(memoList)
} catch (e: Exception) {
_memos.postValue(emptyList())
}
}
}
}
- viewModelScope를 통해 viewmodel 라이프 사이클을 따르도록 하고, Dispatchers.IO를 통해 네트워크 요청을 수행하도록 합니다.
결론
코루틴은 스레드의 좋은 대안으로, 비동기 프로그래밍을 원할하게 해줘 시스템 리소스를 효율적으로 관리할 수 있게 됩니다.
이를 통해 비동기 작업의 가독성을 높이고, 유지보수성을 개선하며 코드를 간결하게 짤 수 있게 도와줍니다.
감사합니다 !!!!
참고 자료
https://kotlinlang.org/docs/coroutines-basics.html#your-first-coroutine
https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/
https://developer.android.com/codelabs/basic-android-kotlin-training-introduction-coroutines?hl=ko#0
https://developer.android.com/kotlin/coroutines?hl=ko#groovy