ABOUT ME

작은 디테일에 집착하는 개발자

Today
-
Yesterday
-
Total
-
  • [Android/Kotlin] 코틀린과 코루틴
    IT Study/Android 2024. 5. 19. 15:08
    728x90

    출처 : kotlinlang.org

     

    코루틴의 개념을 정확하게 이해하고 활용하기 위해, 이를 주제로 글을 작성해보려고 합니다 :) 🍇

     

    목차

    1. Coroutine
    2. Thread와 Coroutine
    3. 그래서 Coroutine이 뭔데?
    4. 코루틴의 구성요소
    4-1. Coroutine Context
    4-2. Coroutine Builder
    4-2-1. launch (main thread unblocking)
    4-2-2. runBlocking (main thread blocking)
    4-2-3. async (main thread unblocking)
    4-3. Coroutine Scope
    5. 주요 함수

     

    1. Coroutine

    우리는 작성된 순서대로 실행되는, 동기적인 실행 방식을 채택해왔다.
    그러나 여러 루틴을 동시 실행하기 위해 비동기 처리를 지원하는 코루틴을 사용할 수 있어야 한다.

    그러나 비동기 처리를 위해 Thread도 사용할 수 있는데, 왜 Kotlin에서는 Coroutine 사용을 권장할까?

     

     

    2. Thread와 Coroutine

    1. 코루틴은 스레드보다 가볍고 적은 비용으로 많이 만들어 사용할 수 있어 관리하기 편하다.
    2. 스레드는 여러 스레드가 동시에 같은 자원에 접근하기 때문에 결과 예측이 어려운 반면, 코루틴은 구조화된 동시성을 통해 결과 예측 가능성을 높이고 오류를 줄일 수 있다.

     

    fun main() {
        var count = 0
        for(i in 1..10) {
            Thread {
                count++
                println("Thread: $i count: $count")
            }.start()
        }
    }
    
    /* 결과
     Thread: 2 count: 2
     Thread: 6 count: 8
     Thread: 5 count: 7
     Thread: 8 count: 10
     Thread: 9 count: 6
     Thread: 4 count: 5
     Thread: 1 count: 4
     Thread: 3 count: 4
     Thread: 10 count: 2
     Thread: 7 count: 10
    */

    위 코드는 여러 스레드에서 동시에 공유 변수 count를 증가시키려는 의도로 작성되었지만,
    경쟁 상태(race condition)를 일으켜 스레드 사용의 단점인 동기화 문제를 보여주는 예시이다.

     

     

    3. 그래서 Coroutine이 뭔데?

    코루틴은 서브 루틴을 일시 정지하고 재개할 수 있는 구성 요소를 말한다. 풀어서 말해, 메인 루틴과 별도로 진행이 가능한 루틴이지만 개발자가 해당 루틴의 실행과 종료를 마음대로 제어할 수 있는 단위이다.

    *서브 루틴 : 반복 기능을 모든 동작으로 main 함수 내에서 실행되는 개별 함수의 흐름
    메인 루틴 : 프로그램 전체의 개괄적 동작으로 main 함수에 의해 수행되는 흐름

     

     

    4. 코루틴의 구성요소

    fun main() = runBlocking {
        val scope = CoroutineScope(Dispatchers.Default)
    
        val job = scope.launch {
            delay(1000L)
            println("world")
        }
        println("hello")
        job.join()
    }
    
    // 결과
    // hello
    // (1초 딜레이)
    // world

    위 코드는 코루틴의 기본 형태를 나타내고 있다. 위 코드를 보며 코루틴의 구성요소에 대해 간단히 집고 넘어가겠다.

     

     

    4-1. Coroutine Context

    job, dispatcher 등은 코루틴 컨텍스트란 코루틴의 실행 환경을 의미한다. job은 launch() 함수로 만든 작업 단위로 코루틴의 실행, 취소, 완료 등의 상태를 확인할 수 있다.

    그렇다면 dispatcher는 뭘까? 전달자, 보내는 역할을 하는 녀석? 코루틴이 사용할 스레드를 결정하는 녀석이다.

    출처 : https://todaycode.tistory.com/182

    dispatcher에는 default, main, io라는 종류가 있다.

    • default : 기본 백그라운드에서 동작 (json parse, 무거운 연산 작업 시)
    • main : 메인(UI) 스레드에서 동작 (화면 UI 작업)
    • io : 네트워크, 디스크 등 I/O에 사용 (네트워크 혹은 DB 작업에 최적화, 스레드 block 시)

     

     

    4-2. Coroutine Builder

    코루틴 빌더는 코루틴을 시작하는 방법 중 하나이다. 가장 기초적으로는 다음 3개의 빌더에 대해 알아보자.
    launch, runBlocking, async.

     

    4-2-1. launch (main thread unblocking)

    새로운 코루틴을 비동기적으로 시작하고 Job 객체를 반환한다. launch를 통해 코루틴의 상태를 관리하고 취소할 수 있다.
    launch는 자신이 호출된 스코프 안에서 실행되며, 스코프가 취소되거나 종료되면 자동 종료된다.

     

    4-2-2. runBlocking (main thread blocking)

    현재 스레드를 차단하며 코루틴을 실행하는 것으로, 메인 스레드에서 간단한 테스트 혹은 비동기 작업을 동기적으로 실행할 때 사용한다. runBlocking 내에서 launch나 async와 같은 다른 코루틴 빌더를 사용할 수 있다.

    runBlocking은 프로덕션 코드에서는 사용이 권장되지 않는다. runBlocking는 코루틴이 종료될 때까지 메인 루틴을 잠시 대기시킨다. 주의할 점은 메인 스레드에서 runBlocking을 걸면, 일정 시간 이상 응답이 없을 경우 ANR이 발생해 앱이 강제 종료될 수 있다.

    fun main() {
        runBlocking {
            launch {
                for (i in 1..5) {
                    print("$i ")
                }
            }
        }
    }
    
    // 결과 : 1 2 3 4 5

     

    4-2-3. async (main thread unblocking)

    launch와 유사하게 코루틴을 시작하지만, Deferred<T> 객체를 반환한다. Deferred<T> 객체는 나중에 결과를 가져올 수 있는 await() 메서드를 제공한다. async는 계산 결과, 데이터 반환해야 할 때 사용된다. 

     

    *Job 객체와 Deferred 객체 반환의 차이가 뭔데?

    Job 객체
    주요 목적은 코루틴의 실행 결과를 저장하거나 반환하지 않는다. 그저, 코루틴의 실행 상태를 관리한다.
    val job = launch {
        // 코루틴 코드
    }
    job.cancel() // 코루틴 취소


    Deferred 객체
    Job의 하위 클래스로, 코루틴이 실행된 결과를 가져올 수 있다. await() 메서드를 통해 코루틴의 결과를 반환받을 수 있다.
    코루틴의 결과는 코루틴 블럭의 마지막 표현식의 결과를 의미한다. 즉, Job 객체 기능을 가지며(실행 상태 관리하며), 추가로 결과 값을 저장 및 반환할 수 있다.

    val deferred = async {
        // 계산이나 데이터 처리
        "Result" // 반환 결과
    }
    val result = deferred.await() // 코루틴의 결과를 기다린 후 반환

     

     

    4-3. Coroutine Scope

    코루틴 스코프란 코루틴이 활동할 수 있는 범위를 말한다. 기본적으로 globalScope와 coroutineScope를 지원한다.
    GlobalScope는 앱의 전체 생명주기를 따르기 때문에, 메모리 누수의 원인이 될 수 있어 사용이 지양된다.
    CoroutineScope는 사용자가 정의하는 스코프로, 특정한 생명주기를 가진 객체와 연결되어 사용될 수 있다.

     

    val scope = CoroutineScope(Dispatchers.Default)
    val coroutineA = scope.launch {  }
    val coroutineB = scope.async {  }

     

    fun main() {
        val scope = GlobalScope
        
        scope.launch {
            for (i in 1..5) {
                println(i)
            }
        }
    }
    
    // 결과 : 출력 없이 즉시 종료

     

     

    5. 주요 함수

    delay(), join(), await()와 같이 루틴의 대기를 위한 함수들, withTimeoutOrNull()과 같이 지정된 시간 안에 코루틴이 완료되지 않으면 null을 반환하는 함수 등이 존재한다.

    • delay(ms: Long) : ms 단위로 코루틴 일시 중지
    • join() : launch로 시작된 코루틴이 종료될 때까지 대기
    • await() : async로 시작된 코루틴이 종료될 때까지 대기
    • withTimeoutOrNull() : 지정된 시간 안에 코루틴이 완료되지 않으면 null 반환
    • cancel() : 코루틴 중단
      • 코루틴 내부의 delay() or yield() 함수가 사용된 위치까지 수행한 뒤 종료
      • cancel()로 인해 속성인 isActive가 false로 되고, 이를 확인해 수동 종료

     

    delay, join 사용 예시

    fun main() {
        runBlocking {
            val a = launch {
                for (i in 1..5) {
                    println(i)
                    delay(10)
                }
            }
    
            val b = async {
                "async 종료"
            }
            
            println("async 대기")
            println(b.await())
    
            println("launch 대기")
            a.join()
            println("launch 종료")
        }
    }
    
    /*
    결과
       
    async 대기
    1
    async 종료
    launch 대기
    2
    3
    4
    5
    launch 종료
    */

     

     

    withTimeoutOrNull 사용 예시

    fun main() {
        runBlocking {
            var result = withTimeoutOrNull(50) {
                for(i in 1..10) {
                    println(i)
                    delay(10)
                }
                "finish"
            }
    
            println(result)
        }
    }
    
    /*
    결과
    
    1
    2
    3
    4
    5
    null
    
    or
    
    1
    2
    3
    4
    null
    */
Designed by Tistory.