코루틴(Coroutine) 간략 설명

|

코루틴의 구성 요소

코틀린은 크게 다음과 같은 요소들로 구성되어 있습니다.

  • CoroutineScope
  • CoroutineContext
  • Dispatcher


CoroutineScope

CoroutineScope는 코루틴이 실행되는 범위입니다. 간편하게 사용하는 GlobalScope의 경우 CoroutineScope의 한 종류입니다.


CoroutineContext

CoroutineContext는 안드로이드에서의 Context와 비슷한 역할을 한다고 생각하면 됩니다. 코루틴을 실행하는 제어 정보가 들어있으며, 주요 요소로는 Job이나 Dispatcher가 있습니다. 코루틴을 사용하다보면 대부분 CoroutineScopeDispatcher 위주로 사용하다보니, CoroutineContext는 직접적으로 사용하는 경우는 적을 수도 있습니다.


Dispatcher

어떤 Thread를 어떻게 동작할 것인지 선택할 수 있습니다. 미리 준비된 Dispatcher 들을 제공하고 있으며 대표적인 예는 다음과 같습니다.

  • Dispatchers.Default: CPU 사용량이 많아서 별도 thread로 분리해야 하는 경우입니다.
  • Dispatchers.IO: Network이나 File 등을 처리할 때 사용합니다.
  • Dispatchers.Main: 기본적으로 사용하지 못하는데, 안드로이드의 경우는 사용 가능합니다. Main UI Thread 에서 동작하도록 할 수 있습니다.


코루틴의 사용 방법

  1. 어떤 Dispatcher를 사용할 것인지 정합니다.
  2. 위에서 정한 Dispatcher를 이용해서 CoroutineScope를 생성합니다.
  3. Coroutine의 launchasync에 동작할 코드 블럭을 전달합니다. launchJob, asyncDeferred 객체를 리턴합니다.
  4. 위에서 리턴한 Job이나 Deffered 객체를 제어합니다. cancel, join 등으로 코루틴을 제어할 수 있습니다.

let 명령어에 else 처리하기

|

예제 코드

fun main(args: Array<String>) {
    var str1: String? = "hello"
    var str2: String? = null

    str1?.let {
        println("str1(1): $str1")
    } ?: run {
        println("str1(2): $str1")
    }

    str2?.let {
        println("str2(1): $str2")
    } ?: run {
        println("str2(2): $str2")
    }
}

결과값은 다음과 같습니다.

str1(1): hello
str2(2): null

코루틴(Coroutine) Pause/Resume 사용하기

|

SAM 변환

import android.os.Bundle
import android.os.Handler
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import kotlinx.android.synthetic.main.activity_main.*
import kotlinx.coroutines.*
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume

class MainActivity : AppCompatActivity() {

    private var continuation: Continuation<String>? = null

    private var count = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        Log.i("snowdeer", "[snowdeer] onCreate()")

        run_button.setOnClickListener {
            startCoroutine()
        }
        next_button.setOnClickListener {
            continuation?.resume("Resumed")
        }

    }

    private fun startThread() {

    }

    private fun startCoroutine() {
        Log.i("snowdeer", "[snowdeer] startCoroutine()")

        GlobalScope.launch {
            for(i in 0 until 5) {
                refreshCount()
                Thread.sleep(500L)
            }

            pause()

            for(i in 0 until 5) {
                refreshCount()
                Thread.sleep(500L)
            }

            pause()

            for(i in 0 until 5) {
                refreshCount()
                Thread.sleep(500L)
            }

            pause()

            for(i in 0 until 5) {
                refreshCount()
                Thread.sleep(500L)
            }

            pause()

            for(i in 0 until 5) {
                refreshCount()
                Thread.sleep(500L)
            }

            pause()
        }

        Log.i("snowdeer", "[snowdeer] end of startCoroutine()")
    }

    private fun refreshCount() {
        count++
        count_textview.text = "$count"
    }

    private suspend fun pause() = suspendCancellableCoroutine<String> {
        continuation = it
    }
}

SAM(Single Abstract Method)

|

SAM 변환

코틀린은 자바에서 작성한 인터페이스에 대해 SAM(Single Abstract Method) 변환을 지원합니다. 그래서 인터페이스를 매개변수로 받는 함수에 대해서 인터페이스 대신 함수를 전달할 수 있고 코드가 간결해집니다.

대표적인 예제로 View.SetOnClickListener()을 들 수 있습니다.


View.SetOnClickListener 예제 (Java)

button.setOnClickListener(new View.OnClickListener() {

    @Override
    public void onClick(View v) {
        // TODO
    }
});


View.SetOnClickListener 예제 (Kotlin)

button.setOnClickListener(object: View.OnClickListener {
    override fun onClick(v: View) {
        // TODO
    }
})


람다를 이용해 인터페이스 대신 함수를 전달하는 코드 (Kotlin)

button.setOnClickListener({ v: View ->
    Unit
    // TODO
})


SAM 변환 후 간소화된 코드 (Kotlin)

button.setOnClickListener {
    // TODO
}

와 같은 형태가 됩니다. 하나의 함수만을 포함하는 인터페이스는 이와 같이 단순하게 표현할 수 있습니다.

범위 지정 함수(let, apply, with, run, also)

|

범위 지정 함수

범위 지정 함수는 중괄호({ })로 묶여 있는 부분에 전체적으로 적용되는 함수이며, 코틀린에서는 let, apply, with, run, also 등과 같은 함수들을 제공하고 있습니다.

이 함수들은 전부 비슷한 기능을 하며, 사용법 또한 비슷합니다.


let

let은 함수를 호출한 객체를 이어지는 블록의 매개변수로 전달하는 역할을 합니다.

함수의 정의는 다음과 같이 되어 있습니다.

fun <T, R> T.let(block: (T) -> R): R


예를 들면 다음과 같은 코드를 작성할 수 있습니다.

fun sample(message:String) {
    message.let {
        Log.i("message: $it")
    }
}

message라는 매개변수를 let으로 다음 블럭에 넘기고, 그 안에서는 it 키워드를 이용해서 message를 사용할 수 있습니다.

다음과 같이 ? 키워드와 같이 사용해서 Null Check 용도로도 사용할 수 있습니다.

fun sample(message:String?) {
    message?.let {
        Log.i("message: $it")
    }
}


apply

applylet과 사용법은 비슷하지만 블럭으로 넘긴 매개변수가 it이 아니라 this라는 점에서 차이가 있습니다.

함수의 정의는 다음과 같습니다.

fun T.apply(block: T.() -> Unit): T


private val timelineGuideLinePaint = Paint()

timelineGuideLinePaint.apply {
    color = context.getColor(R.color.colorTimelineGuideLine)
    style = Paint.Style.STROKE
    pathEffect = DashPathEffect(floatArrayOf(20F, 10F), 20F)
    strokeWidth = 1f
}


with

withapply와 비슷한 용도로 사용됩니다. 다만, with는 인자를 가지며, 해당 인자를 다음 함수 블럭으로 전달합니다.

함수 정의는 다음과 같습니다.

fun <T, R> with(receiver: T, block: T.() -> R): R


fun sample() {
    with(textView) {
        text = "hello, snowdeer"
    }
}


run

run 함수는 인자없이 사용하는 익명 함수처럼 사용하는 방법과 객체에서 호출하는 형태를 제공하고 있습니다.

함수 정의는 다음과 같습니다.

fun run(block: () -> R): R

fun <T, R> T.run(block: T.() -> R): R


val result = run {
    val a = getResult(100, 50)
    val b = getResult(1000, 2000)
    
    a + b
}

fun printName(person: Person) = person.run {
    print("${person.name}")
}

run 내부에서 선언되는 변수들은 블록 외부에 노출되지 않기 때문에 변수 영역을 명확하게 분리할 수 있습니다